Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae95ae6859 | ||
|
|
3efb0edb61 | ||
|
|
c5fdb1a207 | ||
|
|
28f50a750f | ||
|
|
1dd6630311 | ||
|
|
252d026991 | ||
|
|
f9d7b94962 | ||
|
|
b089bf36e3 |
@@ -46,7 +46,10 @@ RUN cd /tmp && \
|
||||
|
||||
# Copy and build nostr_core_lib from project resources
|
||||
COPY resources/nostr_core_lib /build/nostr_core_lib/
|
||||
RUN cd /build/nostr_core_lib && \
|
||||
RUN if [ "$(uname -m)" = "aarch64" ] && ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
|
||||
ln -s "$(command -v gcc)" /usr/local/bin/aarch64-linux-gnu-gcc; \
|
||||
fi && \
|
||||
cd /build/nostr_core_lib && \
|
||||
chmod +x ./build.sh && \
|
||||
./build.sh --nips=1,4,6,19,44
|
||||
|
||||
@@ -54,7 +57,14 @@ RUN cd /build/nostr_core_lib && \
|
||||
COPY src/ /build/src/
|
||||
|
||||
# Build nsigner as a fully static binary
|
||||
RUN gcc -static -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -s -Wall -Wextra -std=c99 \
|
||||
RUN ARCH="$(uname -m)"; \
|
||||
case "$ARCH" in \
|
||||
aarch64|arm64) NOSTR_LIB="/build/nostr_core_lib/libnostr_core_arm64.a" ;; \
|
||||
x86_64|amd64) NOSTR_LIB="/build/nostr_core_lib/libnostr_core_x64.a" ;; \
|
||||
*) echo "Unsupported build arch: $ARCH"; exit 1 ;; \
|
||||
esac; \
|
||||
[ -f "$NOSTR_LIB" ] || { echo "Missing nostr core lib: $NOSTR_LIB"; ls -la /build/nostr_core_lib; exit 1; }; \
|
||||
gcc -static -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -s -Wall -Wextra -std=c99 \
|
||||
-I/build/nostr_core_lib \
|
||||
-I/build/nostr_core_lib/nostr_core \
|
||||
-I/build/nostr_core_lib/cjson \
|
||||
@@ -67,9 +77,10 @@ RUN gcc -static -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -s -Wa
|
||||
/build/src/dispatcher.c \
|
||||
/build/src/policy.c \
|
||||
/build/src/server.c \
|
||||
/build/src/transport_frame.c \
|
||||
/build/src/key_store.c \
|
||||
/build/src/socket_name.c \
|
||||
/build/nostr_core_lib/libnostr_core_x64.a \
|
||||
"$NOSTR_LIB" \
|
||||
-o /build/nsigner_static \
|
||||
$(pkg-config --static --libs libcurl openssl) \
|
||||
-lsecp256k1 -lsqlite3 -lz -lpthread -lm
|
||||
|
||||
1
Makefile
1
Makefile
@@ -18,6 +18,7 @@ SOURCES := \
|
||||
$(SRC_DIR)/dispatcher.c \
|
||||
$(SRC_DIR)/policy.c \
|
||||
$(SRC_DIR)/server.c \
|
||||
$(SRC_DIR)/transport_frame.c \
|
||||
$(SRC_DIR)/key_store.c \
|
||||
$(SRC_DIR)/socket_name.c
|
||||
|
||||
|
||||
24
README.md
24
README.md
@@ -237,6 +237,9 @@ Naming rules:
|
||||
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: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
|
||||
|
||||
@@ -288,6 +291,24 @@ To force a specific socket name (e.g. for scripted clients):
|
||||
nsigner --name my_test_signer
|
||||
```
|
||||
|
||||
Qubes/qrexec service mode (single framed request over stdin/stdout):
|
||||
|
||||
```bash
|
||||
nsigner --listen qrexec
|
||||
```
|
||||
|
||||
Generic stdio transport mode (single framed request over stdin/stdout):
|
||||
|
||||
```bash
|
||||
nsigner --listen stdio
|
||||
```
|
||||
|
||||
TCP transport mode (no TUI; serves requests until terminated):
|
||||
|
||||
```bash
|
||||
nsigner --listen tcp:[::]:8080
|
||||
```
|
||||
|
||||
### 9.2 Send a request (client mode)
|
||||
|
||||
From another terminal, target the signer by its socket name:
|
||||
@@ -365,6 +386,9 @@ Static build:
|
||||
## 11. Document map
|
||||
|
||||
- [`README.md`](README.md): authoritative behavior specification for the foreground single-program model
|
||||
- [`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/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/README.md`](firmware/README.md): firmware-side notes for MCU transport/UI integration
|
||||
|
||||
@@ -105,12 +105,29 @@ echo "Platform: $PLATFORM"
|
||||
echo "Output: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo ""
|
||||
|
||||
if [ "$ARCH" != "$HOST_ARCH" ]; then
|
||||
echo "[0/3] Preparing buildx + QEMU for cross-architecture build"
|
||||
if ! docker buildx inspect >/dev/null 2>&1; then
|
||||
echo "ERROR: docker buildx is not available"
|
||||
exit 1
|
||||
fi
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all >/dev/null
|
||||
|
||||
if ! docker buildx inspect nsigner-builder >/dev/null 2>&1; then
|
||||
docker buildx create --name nsigner-builder --driver docker-container --use >/dev/null
|
||||
else
|
||||
docker buildx use nsigner-builder >/dev/null
|
||||
fi
|
||||
docker buildx inspect --bootstrap >/dev/null
|
||||
fi
|
||||
|
||||
echo "[1/3] Building builder stage from project root context"
|
||||
docker build \
|
||||
docker buildx build \
|
||||
--platform "$PLATFORM" \
|
||||
--target builder \
|
||||
-f "$DOCKERFILE" \
|
||||
-t "$IMAGE_TAG" \
|
||||
--load \
|
||||
"$SCRIPT_DIR"
|
||||
|
||||
echo "[2/3] Extracting static binary"
|
||||
|
||||
@@ -10,7 +10,12 @@ It is written for agent/tool authors implementing robust request flows against t
|
||||
|
||||
## 2. Discovery and socket targeting
|
||||
|
||||
`nsigner` listens on Linux AF_UNIX **abstract namespace** sockets.
|
||||
`nsigner` currently supports two transport families:
|
||||
|
||||
- Linux AF_UNIX **abstract namespace** sockets.
|
||||
- Stdio framed mode (`--listen stdio` and `--listen qrexec`) for one request/response exchange.
|
||||
|
||||
For AF_UNIX:
|
||||
|
||||
- Socket names are exposed in `/proc/net/unix` with a leading `@`.
|
||||
- Typical runtime names: `@nsigner_hairy_dog`, `@nsigner_brave_canyon`.
|
||||
@@ -40,6 +45,15 @@ Expected output format (one per line):
|
||||
|
||||
Clients should accept both `@nsigner` and `@nsigner_*` names.
|
||||
|
||||
### 2.3 Stdio / qrexec mode
|
||||
|
||||
In server mode:
|
||||
|
||||
- `nsigner --listen stdio`: reads exactly one framed request from stdin and writes one framed response to stdout.
|
||||
- `nsigner --listen qrexec`: same behavior, but caller identity may be tagged from `QREXEC_REMOTE_DOMAIN` as `qubes:<vm-name>`.
|
||||
|
||||
This mode is server-side only in the current CLI (the `client` subcommand still targets AF_UNIX).
|
||||
|
||||
---
|
||||
|
||||
## 3. Transport framing
|
||||
177
documents/FIPS_DEPLOYMENT.md
Normal file
177
documents/FIPS_DEPLOYMENT.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# FIPS_DEPLOYMENT.md
|
||||
|
||||
## 1. Scope
|
||||
|
||||
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: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:
|
||||
|
||||
- Mandatory transport-level TLS/authentication hardening (still planned for a later phase).
|
||||
- Automatic caller->npub enrichment from FIPS session metadata.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture
|
||||
|
||||
Two cooperating layers:
|
||||
|
||||
1. **Signer process layer** (`nsigner`)
|
||||
- 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 the configured signer TCP endpoint.
|
||||
|
||||
Conceptually:
|
||||
|
||||
`client app -> local FIPS endpoint -> FIPS mesh -> remote FIPS endpoint -> signer TCP endpoint -> nsigner`
|
||||
|
||||
---
|
||||
|
||||
## 3. Prerequisites
|
||||
|
||||
On signer host/qube:
|
||||
|
||||
- Built `nsigner` binary.
|
||||
- FIPS installed and running.
|
||||
- Local firewall policy that keeps signer listener local-only.
|
||||
|
||||
On caller host/qube:
|
||||
|
||||
- FIPS installed and peered with signer host/qube.
|
||||
- A client implementation that speaks `nsigner` framed JSON-RPC (see `documents/CLIENT_IMPLEMENTATION.md`).
|
||||
|
||||
Operational assumptions:
|
||||
|
||||
- Operator controls both endpoints.
|
||||
- Manual verification of peer identity is performed in FIPS tooling before enabling signer traffic.
|
||||
|
||||
---
|
||||
|
||||
## 4. Start signer in Tier-1 TCP mode
|
||||
|
||||
Run `nsigner` in TCP listen mode:
|
||||
|
||||
```bash
|
||||
./build/nsigner --listen tcp:[::]:8080
|
||||
```
|
||||
|
||||
Or bind to a specific FIPS ULA address:
|
||||
|
||||
```bash
|
||||
./build/nsigner --listen tcp:[fd00::1234]:8080
|
||||
```
|
||||
|
||||
Behavior notes:
|
||||
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## 5. FIPS substrate wiring pattern
|
||||
|
||||
Because FIPS deployment topologies vary, use this generic pattern:
|
||||
|
||||
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 -> configured signer endpoint succeeds.
|
||||
- `nsigner` receives framed request and returns framed response.
|
||||
|
||||
---
|
||||
|
||||
## 6. Minimal validation flow
|
||||
|
||||
### 6.1 Liveness check
|
||||
|
||||
From caller side, send a framed `get_public_key` request through the FIPS-backed endpoint.
|
||||
|
||||
Request JSON:
|
||||
|
||||
```json
|
||||
{"id":"1","method":"get_public_key","params":[]}
|
||||
```
|
||||
|
||||
Expected response:
|
||||
|
||||
```json
|
||||
{"id":"1","result":"<hex_pubkey>"}
|
||||
```
|
||||
|
||||
### 6.2 Signing check
|
||||
|
||||
Send `sign_event` with explicit role selector:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "2",
|
||||
"method": "sign_event",
|
||||
"params": ["<event_json>", {"role":"main"}]
|
||||
}
|
||||
```
|
||||
|
||||
Expected result: signed event JSON in `result`.
|
||||
|
||||
### 6.3 Negative check (policy)
|
||||
|
||||
Trigger a request path that requires prompt/denial and confirm client handles policy denial as a normal result path.
|
||||
|
||||
---
|
||||
|
||||
## 7. Security guardrails
|
||||
|
||||
- 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.
|
||||
- Preserve interactive approval where required by policy.
|
||||
|
||||
---
|
||||
|
||||
## 8. Troubleshooting
|
||||
|
||||
### 8.1 `invalid tcp listen target`
|
||||
|
||||
Cause:
|
||||
- `--listen` argument does not match `tcp:HOST:PORT` or `tcp:[IPv6]:PORT`.
|
||||
|
||||
Fix:
|
||||
- Use valid numeric port and valid IPv4/IPv6 literal host.
|
||||
|
||||
### 8.2 Framing parse failures (`parse_error`)
|
||||
|
||||
Cause:
|
||||
- Client sent line-delimited/raw JSON instead of framed JSON.
|
||||
|
||||
Fix:
|
||||
- Send 4-byte big-endian length prefix followed by exact UTF-8 JSON payload bytes.
|
||||
|
||||
### 8.3 FIPS path up, signer path down
|
||||
|
||||
Cause:
|
||||
- FIPS session exists but forwarding/service mapping to signer loopback endpoint is missing.
|
||||
|
||||
Fix:
|
||||
- Verify substrate service routing config and local endpoint mapping.
|
||||
|
||||
---
|
||||
|
||||
## 9. Next hardening steps (post Tier-1)
|
||||
|
||||
- Add automated two-node validation script for operator smoke checks.
|
||||
- Add optional identity enrichment from FIPS session metadata (`peer_npub`).
|
||||
- Introduce remote TCP mode only with mandatory TLS + authenticated caller key flow.
|
||||
179
documents/QUBES_OS.md
Normal file
179
documents/QUBES_OS.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# QUBES_OS.md
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Run `n_signer` inside a dedicated Qubes OS qube (for example `vault`-like behavior), and let caller qubes access signing via qrexec with explicit policy control.
|
||||
|
||||
This doc outlines what must be implemented/packaged for a reliable Qubes deployment path.
|
||||
|
||||
---
|
||||
|
||||
## 2. Current status (where we are now)
|
||||
|
||||
Implemented in current codebase:
|
||||
|
||||
- `nsigner` supports `--listen qrexec` and `--listen stdio`.
|
||||
- Framing is transport-agnostic and shared via length-prefixed JSON (`4-byte big-endian length + payload`).
|
||||
- In qrexec/stdio mode, server handles one framed request-response exchange.
|
||||
- Caller identity extraction supports `QREXEC_REMOTE_DOMAIN`, surfaced as `qubes:<source-vm>` when available.
|
||||
|
||||
Still missing for complete Qubes integration:
|
||||
|
||||
- qrexec service file + wrapper script artifacts.
|
||||
- dom0 qrexec policy artifacts with sane defaults.
|
||||
- install/uninstall guidance and verification flow for real Qubes deployment.
|
||||
- packaging path (`packaging/qubes/`) and docs wired into README map.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture in Qubes
|
||||
|
||||
### 3.1 Components
|
||||
|
||||
- **Signer qube** (target): runs `nsigner` service entrypoint.
|
||||
- **Caller qube(s)**: apps/tools invoking qrexec service.
|
||||
- **dom0 policy**: controls which caller qubes may invoke signer service.
|
||||
|
||||
### 3.2 Request path
|
||||
|
||||
1. Caller qube invokes qrexec service (e.g. `qubes.NsignerRpc`).
|
||||
2. qrexec starts service command inside signer qube.
|
||||
3. Service command runs `nsigner --listen qrexec`.
|
||||
4. Caller sends framed JSON-RPC request over qrexec stdio channel.
|
||||
5. `nsigner` returns framed JSON-RPC response.
|
||||
|
||||
### 3.3 Trust and identity
|
||||
|
||||
- Source qube identity comes from `QREXEC_REMOTE_DOMAIN`.
|
||||
- `n_signer` maps caller as `qubes:<source-vm>` where available.
|
||||
- qrexec policy in dom0 remains first enforcement boundary.
|
||||
- `n_signer` policy/approval remains second boundary.
|
||||
|
||||
---
|
||||
|
||||
## 4. Required implementation tasks
|
||||
|
||||
## 4.1 Service entrypoint artifacts ✅ Implemented
|
||||
|
||||
Implemented repo artifacts:
|
||||
|
||||
- `packaging/qubes/rpc/qubes.NsignerRpc`
|
||||
- `packaging/qubes/install-service.sh`
|
||||
|
||||
`qubes.NsignerRpc` runs:
|
||||
|
||||
- `exec /usr/local/bin/nsigner --listen qrexec`
|
||||
|
||||
Install inside the signer qube:
|
||||
|
||||
```bash
|
||||
sudo sh packaging/qubes/install-service.sh
|
||||
```
|
||||
|
||||
This installs the qrexec service to `/etc/qubes-rpc/qubes.NsignerRpc` with executable permissions.
|
||||
|
||||
## 4.2 dom0 policy artifacts ✅ Implemented
|
||||
|
||||
Implemented repo artifacts:
|
||||
|
||||
- `packaging/qubes/policy.d/40-nsigner.policy`
|
||||
- `packaging/qubes/install-policy.sh`
|
||||
|
||||
Policy defaults now use explicit `ask` plus deny catch-all:
|
||||
|
||||
- `qubes.NsignerRpc * @anyvm @tag:nsigner-signer ask default_target=nsigner-vault`
|
||||
- `qubes.NsignerRpc * @anyvm @anyvm deny`
|
||||
|
||||
Install in dom0:
|
||||
|
||||
```bash
|
||||
sudo sh packaging/qubes/install-policy.sh
|
||||
```
|
||||
|
||||
This installs `/etc/qubes/policy.d/40-nsigner.policy` and prints signer-tag guidance.
|
||||
|
||||
## 4.3 Policy model inside n_signer for qubes callers ✅ Implemented
|
||||
|
||||
Current code reads caller as `qubes:<vm>` and qrexec default behavior is hardened.
|
||||
|
||||
In qrexec mode, default prompt behavior is now:
|
||||
|
||||
- `PROMPT_EVERY_REQUEST`
|
||||
|
||||
This replaces the previous permissive `PROMPT_NEVER` temporary setting.
|
||||
|
||||
## 4.4 Client helper examples ✅ Implemented
|
||||
|
||||
Added:
|
||||
|
||||
- `documents/qubes_client_examples.md`
|
||||
|
||||
Includes:
|
||||
|
||||
- shell helper example invoking `qrexec-client-vm` with framed request/response handling
|
||||
- Python helper example implementing frame encode/decode over qrexec stdio channel
|
||||
- reference to `documents/CLIENT_IMPLEMENTATION.md` for full protocol details
|
||||
|
||||
---
|
||||
|
||||
## 5. Operational runbook
|
||||
|
||||
## 5.1 Setup signer qube
|
||||
|
||||
- install `nsigner` binary at `/usr/local/bin/nsigner`
|
||||
- run `sudo sh packaging/qubes/install-service.sh`
|
||||
- verify `/etc/qubes-rpc/qubes.NsignerRpc` exists and is executable
|
||||
|
||||
## 5.2 Setup dom0 policy
|
||||
|
||||
- run `sudo sh packaging/qubes/install-policy.sh`
|
||||
- tag signer qube (example): `qvm-tags nsigner-vault add nsigner-signer`
|
||||
- reload qrexec policy per Qubes procedure/version
|
||||
|
||||
## 5.3 Verification
|
||||
|
||||
- from caller qube, invoke test request (`get_public_key`)
|
||||
- confirm signer qube receives request
|
||||
- confirm activity log displays `qubes:<source-vm>` caller prefix
|
||||
- validate deny behavior from unauthorized qube
|
||||
|
||||
## 5.4 Failure checks
|
||||
|
||||
- malformed frame -> parse error response
|
||||
- missing policy -> deny path
|
||||
- missing `QREXEC_REMOTE_DOMAIN` -> fallback identity path
|
||||
|
||||
---
|
||||
|
||||
## 6. Security requirements
|
||||
|
||||
- Never run signer service in disposable qube if mnemonic persistence is expected.
|
||||
- Prefer dedicated minimal template for signer qube.
|
||||
- Keep qrexec policy narrowly scoped (explicit source + target).
|
||||
- Require user approval for sensitive methods unless explicitly intended otherwise.
|
||||
- Log caller identity and method (without secret payload logging).
|
||||
|
||||
---
|
||||
|
||||
## 7. Documentation tasks
|
||||
|
||||
Update these after packaging lands:
|
||||
|
||||
- `README.md`
|
||||
- add Qubes deployment subsection under transport/usage
|
||||
- add `documents/QUBES_OS.md` and moved `documents/CLIENT_IMPLEMENTATION.md` in document map
|
||||
- `plans/nsigner.md`
|
||||
- mark T1 done with packaging status clearly separated
|
||||
|
||||
---
|
||||
|
||||
## 8. Definition of done (Qubes)
|
||||
|
||||
Qubes integration is considered complete when:
|
||||
|
||||
1. qrexec service artifact exists and is installable.
|
||||
2. dom0 policy artifact exists with secure default pattern.
|
||||
3. End-to-end call from allowed caller qube succeeds.
|
||||
4. Call from unauthorized qube is denied.
|
||||
5. Caller displayed as `qubes:<vm>` in activity.
|
||||
6. README + docs include full setup and troubleshooting.
|
||||
93
documents/qubes_client_examples.md
Normal file
93
documents/qubes_client_examples.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# qubes_client_examples.md
|
||||
|
||||
This document shows minimal caller-qube examples for invoking `nsigner --listen qrexec` through Qubes qrexec.
|
||||
|
||||
For complete protocol details (framing, JSON-RPC, error handling), see `documents/CLIENT_IMPLEMENTATION.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1) Shell example (`qrexec-client-vm` + framed JSON)
|
||||
|
||||
This sends one `get_public_key` request and decodes one framed response.
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
TARGET_QUBE="nsigner-vault"
|
||||
SERVICE="qubes.NsignerRpc"
|
||||
REQ='{"id":"1","method":"get_public_key","params":[]}'
|
||||
|
||||
python3 - "$TARGET_QUBE" "$SERVICE" "$REQ" <<'PY'
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
target, service, req_json = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
frame = struct.pack(">I", len(req_json.encode("utf-8"))) + req_json.encode("utf-8")
|
||||
|
||||
p = subprocess.Popen(
|
||||
["qrexec-client-vm", target, service],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
out, _ = p.communicate(frame)
|
||||
if p.returncode != 0:
|
||||
raise SystemExit(f"qrexec-client-vm failed: {p.returncode}")
|
||||
if len(out) < 4:
|
||||
raise SystemExit("short response (missing frame header)")
|
||||
|
||||
n = struct.unpack(">I", out[:4])[0]
|
||||
payload = out[4:4+n]
|
||||
if len(payload) != n:
|
||||
raise SystemExit("short response payload")
|
||||
|
||||
print(json.dumps(json.loads(payload.decode("utf-8")), indent=2))
|
||||
PY
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2) Python example (explicit frame helpers over qrexec stdio)
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
|
||||
|
||||
def frame_encode(obj: dict) -> bytes:
|
||||
payload = json.dumps(obj, separators=(",", ":")).encode("utf-8")
|
||||
return struct.pack(">I", len(payload)) + payload
|
||||
|
||||
|
||||
def frame_decode(buf: bytes) -> dict:
|
||||
if len(buf) < 4:
|
||||
raise ValueError("missing frame header")
|
||||
n = struct.unpack(">I", buf[:4])[0]
|
||||
payload = buf[4:4 + n]
|
||||
if len(payload) != n:
|
||||
raise ValueError("short frame payload")
|
||||
return json.loads(payload.decode("utf-8"))
|
||||
|
||||
|
||||
def call_nsigner_qrexec(target_qube: str, request: dict) -> dict:
|
||||
proc = subprocess.Popen(
|
||||
["qrexec-client-vm", target_qube, "qubes.NsignerRpc"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
out, err = proc.communicate(frame_encode(request))
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"qrexec failed ({proc.returncode}): {err.decode('utf-8', 'replace')}")
|
||||
return frame_decode(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
req = {"id": "1", "method": "get_public_key", "params": []}
|
||||
resp = call_nsigner_qrexec("nsigner-vault", req)
|
||||
print(json.dumps(resp, indent=2))
|
||||
```
|
||||
@@ -244,13 +244,7 @@ main() {
|
||||
check_git_repo
|
||||
|
||||
if [[ "$RELEASE_MODE" == true ]]; then
|
||||
if [[ "$VERSION_INCREMENT_EXPLICIT" == true ]]; then
|
||||
increment_version "$VERSION_INCREMENT_TYPE"
|
||||
else
|
||||
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "v0.0.1")
|
||||
NEW_VERSION="$LATEST_TAG"
|
||||
export NEW_VERSION
|
||||
fi
|
||||
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
|
||||
223
install_qube_fips_nsigner.sh
Executable file
223
install_qube_fips_nsigner.sh
Executable file
@@ -0,0 +1,223 @@
|
||||
#!/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=vX.Y.Z # optional override; default is latest release tag
|
||||
# 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:-}"
|
||||
|
||||
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=vX.Y.Z # optional override; default is latest release tag
|
||||
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}"
|
||||
}
|
||||
|
||||
resolve_nsigner_version() {
|
||||
local headers=()
|
||||
local latest_tag=""
|
||||
|
||||
if [[ -n "${NSIGNER_VERSION}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "${NSIGNER_GITEA_TOKEN:-}" ]]; then
|
||||
headers=(-H "Authorization: token ${NSIGNER_GITEA_TOKEN}")
|
||||
fi
|
||||
|
||||
latest_tag="$(curl -fsSL "${headers[@]}" "https://git.laantungir.net/api/v1/repos/laantungir/n_signer/releases" \
|
||||
| jq -r '.[0].tag_name // empty' || true)"
|
||||
|
||||
if [[ -z "${latest_tag}" ]]; then
|
||||
err "Could not resolve latest n_signer release tag from API."
|
||||
err "Set NSIGNER_VERSION explicitly (e.g. NSIGNER_VERSION=v0.0.11)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NSIGNER_VERSION="${latest_tag}"
|
||||
}
|
||||
|
||||
download_nsigner_asset_url() {
|
||||
local headers=()
|
||||
local api_tag_url="https://git.laantungir.net/api/v1/repos/laantungir/n_signer/releases/tags/${NSIGNER_VERSION}"
|
||||
|
||||
if [[ -n "${NSIGNER_GITEA_TOKEN:-}" ]]; then
|
||||
headers=(-H "Authorization: token ${NSIGNER_GITEA_TOKEN}")
|
||||
fi
|
||||
|
||||
curl -fsSL "${headers[@]}" "${api_tag_url}" \
|
||||
| jq -r '.assets[]?.browser_download_url // empty' \
|
||||
| grep -E 'nsigner_static_x86_64$' \
|
||||
| head -n1 || true
|
||||
}
|
||||
|
||||
install_nsigner() {
|
||||
local release_page=""
|
||||
|
||||
resolve_nsigner_version
|
||||
release_page="https://git.laantungir.net/laantungir/n_signer/releases/tag/${NSIGNER_VERSION}"
|
||||
|
||||
log "Installing n_signer ${NSIGNER_VERSION}"
|
||||
log "Release page: ${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 "$@"
|
||||
17
packaging/qubes/install-policy.sh
Normal file
17
packaging/qubes/install-policy.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
POLICY_SRC="packaging/qubes/policy.d/40-nsigner.policy"
|
||||
POLICY_DST="/etc/qubes/policy.d/40-nsigner.policy"
|
||||
|
||||
if [ ! -f "$POLICY_SRC" ]; then
|
||||
echo "Missing policy source: $POLICY_SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -m 0644 "$POLICY_SRC" "$POLICY_DST"
|
||||
|
||||
echo "Installed qrexec policy to $POLICY_DST"
|
||||
echo "Tag your signer qube in dom0, for example:"
|
||||
echo " qvm-tags nsigner-vault add nsigner-signer"
|
||||
echo "Then reload policy per your Qubes OS version procedures."
|
||||
15
packaging/qubes/install-service.sh
Normal file
15
packaging/qubes/install-service.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
SERVICE_SRC="packaging/qubes/rpc/qubes.NsignerRpc"
|
||||
SERVICE_DST="/etc/qubes-rpc/qubes.NsignerRpc"
|
||||
|
||||
if [ ! -f "$SERVICE_SRC" ]; then
|
||||
echo "Missing service source: $SERVICE_SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -m 0755 "$SERVICE_SRC" "$SERVICE_DST"
|
||||
|
||||
echo "Installed qrexec service to $SERVICE_DST"
|
||||
echo "Executable bit set via install -m 0755."
|
||||
5
packaging/qubes/policy.d/40-nsigner.policy
Normal file
5
packaging/qubes/policy.d/40-nsigner.policy
Normal file
@@ -0,0 +1,5 @@
|
||||
# Qubes OS qrexec policy for nsigner
|
||||
# Syntax: service +argument source target action
|
||||
# Allow specific qubes to reach the signer qube with user confirmation
|
||||
qubes.NsignerRpc * @anyvm @tag:nsigner-signer ask default_target=nsigner-vault
|
||||
qubes.NsignerRpc * @anyvm @anyvm deny
|
||||
2
packaging/qubes/rpc/qubes.NsignerRpc
Normal file
2
packaging/qubes/rpc/qubes.NsignerRpc
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
exec /usr/local/bin/nsigner --listen qrexec
|
||||
116
plans/nsigner.md
116
plans/nsigner.md
@@ -174,3 +174,119 @@ Privacy/UX notes:
|
||||
- Expand integration coverage for NIP-04/NIP-44 edge cases and negative-path errors.
|
||||
- Document and test static artifact size budgets across targets.
|
||||
- Define MCU transport adapter contract to prepare desktop/firmware parity.
|
||||
|
||||
## 7. Transport expansion roadmap
|
||||
|
||||
Goal: keep one signer core, swap transports underneath without touching dispatcher, policy, or role layers. The wire contract in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md) (4-byte length-prefixed JSON-RPC) stays identical across every transport; only listener and `caller_identity_t` change.
|
||||
|
||||
### 7.0 Prerequisite — transport abstraction (Phase T0)
|
||||
|
||||
Before adding any new transport, factor a small adapter contract out of [`src/server.c`](../src/server.c) and [`src/main.c`](../src/main.c).
|
||||
|
||||
- New header `src/transport.h` declaring an opaque `nsigner_transport_t` with:
|
||||
- `accept(listener) -> connection`
|
||||
- `recv_frame(connection) -> bytes`
|
||||
- `send_frame(connection, bytes)`
|
||||
- `peer_identity(connection) -> caller_identity_t`
|
||||
- `close(connection)` / `shutdown(listener)`
|
||||
- Generalize `caller_identity_t` to a tagged union of:
|
||||
- `unix_peer { uid, pid, comm }` (current behavior)
|
||||
- `qubes { source_qube_name }`
|
||||
- `tcp_local { addr }`
|
||||
- `tcp_remote { addr, authenticated_pubkey }`
|
||||
- `fips { peer_npub }`
|
||||
- `usb_serial { device_path, asserted_caller }`
|
||||
- Move `recv_framed` / `send_framed` from `server.c` and `main.c` into a single shared `transport_frame.c` so client and server share one framing implementation.
|
||||
- Server main loop becomes transport-agnostic (`while accept; recv; dispatch; send`).
|
||||
- Tests: extend [`tests/test_integration.c`](../tests/test_integration.c) with a transport-loopback fake to validate the abstraction without binding any real socket.
|
||||
|
||||
This refactor is purely internal — no observable change.
|
||||
|
||||
### 7.1 Phase T1 — Qubes OS qrexec transport
|
||||
|
||||
Use Qubes' native inter-qube primitive instead of inventing one.
|
||||
|
||||
- Add a qrexec service script (e.g. `qubes.NsignerRpc`) that execs `nsigner` in a "stdio transport" mode where stdin/stdout carry the existing length-prefixed frame protocol.
|
||||
- New CLI: `nsigner --listen stdio` (and `nsigner --listen qrexec`, behaving identically; `qrexec` value is for documentation/intent).
|
||||
- Caller identity comes from qrexec environment (`QREXEC_REMOTE_DOMAIN`) and is mapped to `caller_identity_t.kind=qubes`.
|
||||
- Reference policy file under `packaging/qubes/policy.d/40-nsigner.policy` showing `ask` / `allow` per source qube.
|
||||
- No new attack surface inside nsigner: dom0 enforces who can even invoke the service.
|
||||
- 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 transport
|
||||
|
||||
Smallest IP-based step; on-ramp for non-Linux clients and for FIPS later.
|
||||
|
||||
- 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` (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] 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.
|
||||
|
||||
### 7.3 Phase T3 — TCP remote with TLS + caller-pubkey auth
|
||||
|
||||
Only after T2 is solid.
|
||||
|
||||
- New CLI: `nsigner --listen tcp:0.0.0.0:PORT --allow-remote --tls-cert <pem> --tls-key <pem>`.
|
||||
- Mandatory: TLS for any non-loopback bind. Refuse to start otherwise.
|
||||
- Caller authentication: client must sign a per-connection challenge with its declared npub (Schnorr/secp256k1) before any signer verb is dispatched. Identity becomes `tcp_remote { addr, authenticated_pubkey }`.
|
||||
- Failure modes: `transport_tls_required`, `caller_auth_failed`, `caller_auth_timeout` — all surfaced with new error names in dispatcher and documented in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md).
|
||||
- Approval prompt now displays `caller=npub:abcd…wxyz` instead of `uid:1000`.
|
||||
- Tests: integration test that exercises happy path, wrong-pubkey, replayed-challenge, expired-challenge.
|
||||
- Docs: dedicated "Remote TCP deployment" section in `README.md` with strong "do not expose to the public internet without firewalling" warning.
|
||||
|
||||
### 7.4 Phase T4 — FIPS substrate integration
|
||||
|
||||
FIPS is a *substrate* for an existing TCP listener, not a new transport in nsigner code.
|
||||
|
||||
- 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).
|
||||
- Tests: a Docker-compose fixture borrowed from `resources/fips/testing/` that boots two FIPS nodes, runs nsigner on one, runs a Python client (per snippet in [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md)) on the other, and exercises the same verb matrix.
|
||||
- Docs: new [`documents/FIPS_DEPLOYMENT.md`](../documents/FIPS_DEPLOYMENT.md) deep-dive describing identity mapping, npub-as-caller, and operator setup. Cross-link from [`README.md`](../README.md) section 7 (Transport).
|
||||
|
||||
Execution tasks for initial FIPS trial:
|
||||
|
||||
- [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.
|
||||
|
||||
### 7.5 Phase T5 — USB / serial transport
|
||||
|
||||
Two distinct sub-tracks; do not conflate.
|
||||
|
||||
- T5a (firmware-side, MCU): ESP32/USB-CDC. Already in the [`firmware/`](../firmware/) track. Same dispatcher; transport adapter is UART read/write loop. `caller_identity_t.kind=usb_serial` with `asserted_caller` because the host claims the identity.
|
||||
- T5b (host-side optional): `nsigner --listen serial:/dev/ttyACM0,baud=115200`. Useful for desktop signer reachable by a USB-tethered client. Same frame protocol over the serial line. Marks identity as asserted (low trust) and forces approval prompt.
|
||||
- USB-as-Ethernet (gadget mode, RNDIS/ECM) is **not** a separate transport — it reduces to T2/T3.
|
||||
- Tests: loopback pty pair (`openpty`) for T5b unit/integration coverage; firmware-side covered in firmware track.
|
||||
|
||||
### 7.6 Cross-cutting concerns
|
||||
|
||||
Apply once per phase as needed:
|
||||
|
||||
- Transport-aware approval prompt: clear visual indication of transport kind and identity (uid vs qube vs npub vs serial-asserted). No silent identity-source confusion.
|
||||
- Per-transport policy gates: deny-by-default for new identity kinds until operator explicitly enables them in policy.
|
||||
- Discovery (`nsigner list`) becomes per-transport pluggable (proc/net/unix today, internal registry for tcp, qrexec service announce for qubes, fips peer table for fips).
|
||||
- Audit logging: include transport kind and identity descriptor in every approval/decision record.
|
||||
- Error name parity: every new transport introduces only well-named errors (extend the table in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md) section 5).
|
||||
|
||||
### 7.7 Decision points (open)
|
||||
|
||||
- D1: Land T0 (refactor) before any transport, or in parallel with T1?
|
||||
- D2: Bundle T2 and T3 as one phase, or hard split (loopback-only first, then remote-with-TLS later)?
|
||||
- D3: T4 FIPS — embed an explicit `caller_kind=fips` path in nsigner now, or treat FIPS as plain TCP and revisit identity enrichment after a working deployment?
|
||||
- D4: T5b host-side serial — in scope for desktop nsigner, or strictly firmware track?
|
||||
- D5: Qubes packaging — ship `packaging/qubes/` artifacts in this repo, or document only and let operators wire it up?
|
||||
|
||||
340
src/main.c
340
src/main.c
@@ -373,12 +373,19 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
|
||||
#define SERVER_SOCKET_NAME_MAX 108
|
||||
#define SERVER_MAX_MSG_SIZE 65536
|
||||
|
||||
#define NSIGNER_LISTEN_UNIX 0
|
||||
#define NSIGNER_LISTEN_STDIO 1
|
||||
#define NSIGNER_LISTEN_QREXEC 2
|
||||
#define NSIGNER_LISTEN_TCP 3
|
||||
|
||||
/* Caller identity */
|
||||
typedef struct {
|
||||
uid_t uid;
|
||||
gid_t gid;
|
||||
pid_t pid;
|
||||
char caller_id[64]; /* "uid:<n>" */
|
||||
int kind;
|
||||
char caller_id[64]; /* "uid:<n>" or "qubes:<vm>" */
|
||||
char source_qube[64];
|
||||
} caller_identity_t;
|
||||
|
||||
/* Server context */
|
||||
@@ -387,6 +394,8 @@ typedef struct {
|
||||
char last_error[256];
|
||||
int listen_fd;
|
||||
int running;
|
||||
int listen_mode;
|
||||
int stdio_handled;
|
||||
dispatcher_ctx_t *dispatcher;
|
||||
policy_table_t *policy;
|
||||
int socket_name_explicit;
|
||||
@@ -395,6 +404,7 @@ typedef struct {
|
||||
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
|
||||
* socket_name_explicit should be non-zero when provided via --socket-name override. */
|
||||
void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit,
|
||||
int listen_mode,
|
||||
dispatcher_ctx_t *dispatcher, policy_table_t *policy);
|
||||
|
||||
/* Start listening. Returns 0 on success, -1 on error. */
|
||||
@@ -441,12 +451,15 @@ 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 5
|
||||
#define NSIGNER_VERSION "v0.0.5"
|
||||
#define NSIGNER_VERSION_PATCH 12
|
||||
#define NSIGNER_VERSION "v0.0.12"
|
||||
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
|
||||
int transport_send_framed(int fd, const char *payload);
|
||||
int transport_recv_framed(int fd, char **out_payload, size_t max_size);
|
||||
|
||||
#include <nostr_core/nostr_common.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
@@ -462,6 +475,7 @@ int socket_name_random(char *out, size_t out_len);
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <termios.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define NSIGNER_DEFAULT_SOCKET_NAME "nsigner"
|
||||
@@ -501,100 +515,6 @@ static int read_line_stdin(char *buf, size_t buf_sz) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_full(int fd, void *buf, size_t len) {
|
||||
unsigned char *p = (unsigned char *)buf;
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = read(fd, p + off, len - off);
|
||||
if (n == 0) {
|
||||
return -1;
|
||||
}
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int write_full(int fd, const void *buf, size_t len) {
|
||||
const unsigned char *p = (const unsigned char *)buf;
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = write(fd, p + off, len - off);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_framed(int fd, const char *payload) {
|
||||
uint32_t len;
|
||||
uint32_t be_len;
|
||||
|
||||
if (payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = (uint32_t)strlen(payload);
|
||||
be_len = htonl(len);
|
||||
|
||||
if (write_full(fd, &be_len, sizeof(be_len)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (write_full(fd, payload, len) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int recv_framed(int fd, char **out_payload) {
|
||||
uint32_t be_len;
|
||||
uint32_t len;
|
||||
char *payload;
|
||||
|
||||
if (out_payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_payload = NULL;
|
||||
|
||||
if (read_full(fd, &be_len, sizeof(be_len)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = ntohl(be_len);
|
||||
if (len == 0 || len > SERVER_MAX_MSG_SIZE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload = (char *)malloc((size_t)len + 1U);
|
||||
if (payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (read_full(fd, payload, len) != 0) {
|
||||
free(payload);
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload[len] = '\0';
|
||||
*out_payload = payload;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int connect_abstract_socket(const char *name) {
|
||||
int fd;
|
||||
@@ -628,7 +548,8 @@ static int connect_abstract_socket(const char *name) {
|
||||
static void print_usage(const char *program_name) {
|
||||
printf("nsigner - single-binary signer program\n");
|
||||
printf("Usage:\n");
|
||||
printf(" %s [--socket-name|--name|-n <name>] Run signer server + built-in TUI\n", program_name);
|
||||
printf(" %s [--socket-name|--name|-n <name>] [--listen <unix|stdio|qrexec|tcp:HOST:PORT>]\n", program_name);
|
||||
printf(" Run signer server (unix mode has TUI)\n");
|
||||
printf(" %s [--socket-name|--name|-n <name>] client '<json>' Send JSON-RPC request\n", program_name);
|
||||
printf(" %s [--socket-name|--name|-n <name>] client - Read JSON-RPC request from stdin\n", program_name);
|
||||
printf(" %s list List running nsigner abstract sockets\n", program_name);
|
||||
@@ -778,13 +699,13 @@ static int client_main(int argc, char *argv[], const char *socket_name, int sock
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (send_framed(fd, request) != 0) {
|
||||
if (transport_send_framed(fd, request) != 0) {
|
||||
perror("send");
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (recv_framed(fd, &response) != 0) {
|
||||
if (transport_recv_framed(fd, &response, SERVER_MAX_MSG_SIZE) != 0) {
|
||||
perror("recv");
|
||||
close(fd);
|
||||
return 1;
|
||||
@@ -799,18 +720,54 @@ static int client_main(int argc, char *argv[], const char *socket_name, int sock
|
||||
|
||||
static void activity_log_cb(const char *message, void *user_data) {
|
||||
int idx;
|
||||
time_t now;
|
||||
struct tm tm_now;
|
||||
char ts[16];
|
||||
|
||||
(void)user_data;
|
||||
if (message == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
now = time(NULL);
|
||||
if (localtime_r(&now, &tm_now) != NULL) {
|
||||
(void)strftime(ts, sizeof(ts), "%m%d-%H%M%S", &tm_now);
|
||||
} else {
|
||||
strncpy(ts, "0000-000000", sizeof(ts) - 1);
|
||||
ts[sizeof(ts) - 1] = '\0';
|
||||
}
|
||||
|
||||
idx = g_activity_log.count % ACTIVITY_LOG_CAP;
|
||||
strncpy(g_activity_log.lines[idx], message, sizeof(g_activity_log.lines[idx]) - 1);
|
||||
g_activity_log.lines[idx][sizeof(g_activity_log.lines[idx]) - 1] = '\0';
|
||||
(void)snprintf(g_activity_log.lines[idx],
|
||||
sizeof(g_activity_log.lines[idx]),
|
||||
"%s %s",
|
||||
ts,
|
||||
message);
|
||||
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,
|
||||
@@ -850,7 +807,7 @@ static void render_status(const role_table_t *role_table,
|
||||
}
|
||||
|
||||
printf("\nHotkeys\n-------\n");
|
||||
printf("q quit l lock/reunlock r refresh a toggle auto-approve(prompt): %s\n",
|
||||
printf("q/x quit l lock/reunlock r refresh a toggle auto-approve(prompt): %s\n",
|
||||
g_auto_approve ? "ON" : "OFF");
|
||||
fflush(stdout);
|
||||
}
|
||||
@@ -878,22 +835,41 @@ static int setup_default_role(role_table_t *role_table) {
|
||||
static int prompt_load_mnemonic(mnemonic_state_t *mnemonic) {
|
||||
char phrase[MNEMONIC_MAX_LEN];
|
||||
char phrase_copy[MNEMONIC_MAX_LEN];
|
||||
char mode[16];
|
||||
struct termios old_term;
|
||||
struct termios new_term;
|
||||
int have_term = 0;
|
||||
char mode[MNEMONIC_MAX_LEN];
|
||||
int invalid_attempts = 0;
|
||||
const int max_invalid_attempts = 10;
|
||||
|
||||
if (mnemonic == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("Mnemonic source: [E]nter existing or [G]enerate new (default E): ");
|
||||
while (invalid_attempts < max_invalid_attempts) {
|
||||
printf("Mnemonic source: [E]nter existing or [G]enerate new (default E; you can also paste mnemonic here): ");
|
||||
fflush(stdout);
|
||||
if (read_line_stdin(mode, sizeof(mode)) != 0) {
|
||||
fprintf(stderr, "Failed to read mnemonic source choice\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ((mode[0] == 'q' || mode[0] == 'Q' || mode[0] == 'x' || mode[0] == 'X') && mode[1] == '\0') {
|
||||
fprintf(stderr, "User requested exit.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strchr(mode, ' ') != NULL && mode[0] != 'g' && mode[0] != 'G') {
|
||||
if (mnemonic_load(mnemonic, mode) == 0) {
|
||||
printf("Seed phrase is valid and accepted.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
invalid_attempts++;
|
||||
fprintf(stderr,
|
||||
"Invalid mnemonic (must be 12/15/18/21/24 words). Attempts: %d/%d\n",
|
||||
invalid_attempts,
|
||||
max_invalid_attempts);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode[0] == 'g' || mode[0] == 'G') {
|
||||
int idx = 1;
|
||||
char *ctx = NULL;
|
||||
@@ -922,6 +898,7 @@ static int prompt_load_mnemonic(mnemonic_state_t *mnemonic) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("Seed phrase is valid and accepted.\n");
|
||||
memset(phrase, 0, sizeof(phrase));
|
||||
memset(phrase_copy, 0, sizeof(phrase_copy));
|
||||
return 0;
|
||||
@@ -930,35 +907,33 @@ static int prompt_load_mnemonic(mnemonic_state_t *mnemonic) {
|
||||
printf("Enter mnemonic (12/15/18/21/24 words): ");
|
||||
fflush(stdout);
|
||||
|
||||
if (isatty(STDIN_FILENO) && tcgetattr(STDIN_FILENO, &old_term) == 0) {
|
||||
new_term = old_term;
|
||||
new_term.c_lflag &= (tcflag_t)~ECHO;
|
||||
if (tcsetattr(STDIN_FILENO, TCSANOW, &new_term) == 0) {
|
||||
have_term = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (read_line_stdin(phrase, sizeof(phrase)) != 0) {
|
||||
if (have_term) {
|
||||
(void)tcsetattr(STDIN_FILENO, TCSANOW, &old_term);
|
||||
}
|
||||
fprintf(stderr, "Failed to read mnemonic\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (have_term) {
|
||||
(void)tcsetattr(STDIN_FILENO, TCSANOW, &old_term);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
if (mnemonic_load(mnemonic, phrase) != 0) {
|
||||
if ((phrase[0] == 'q' || phrase[0] == 'Q' || phrase[0] == 'x' || phrase[0] == 'X') && phrase[1] == '\0') {
|
||||
memset(phrase, 0, sizeof(phrase));
|
||||
fprintf(stderr, "Invalid mnemonic (must be 12/15/18/21/24 words)\n");
|
||||
fprintf(stderr, "User requested exit.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mnemonic_load(mnemonic, phrase) == 0) {
|
||||
printf("Seed phrase is valid and accepted.\n");
|
||||
memset(phrase, 0, sizeof(phrase));
|
||||
return 0;
|
||||
}
|
||||
|
||||
memset(phrase, 0, sizeof(phrase));
|
||||
invalid_attempts++;
|
||||
fprintf(stderr,
|
||||
"Invalid mnemonic (must be 12/15/18/21/24 words). Attempts: %d/%d\n",
|
||||
invalid_attempts,
|
||||
max_invalid_attempts);
|
||||
}
|
||||
|
||||
fprintf(stderr, "Too many invalid mnemonic attempts (%d). Exiting.\n", max_invalid_attempts);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void apply_test_overrides(policy_table_t *policy) {
|
||||
@@ -1017,6 +992,8 @@ int main(int argc, char *argv[]) {
|
||||
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;
|
||||
const char *listen_target = NSIGNER_DEFAULT_SOCKET_NAME;
|
||||
int argi = 1;
|
||||
|
||||
while (argi < argc) {
|
||||
@@ -1032,14 +1009,43 @@ int main(int argc, char *argv[]) {
|
||||
argi += 2;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(argv[argi], "--listen") == 0) {
|
||||
if (argi + 1 >= argc) {
|
||||
fprintf(stderr, "Missing value for %s\n", argv[argi]);
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(argv[argi + 1], "unix") == 0) {
|
||||
listen_mode = NSIGNER_LISTEN_UNIX;
|
||||
} else if (strcmp(argv[argi + 1], "stdio") == 0) {
|
||||
listen_mode = NSIGNER_LISTEN_STDIO;
|
||||
} else if (strcmp(argv[argi + 1], "qrexec") == 0) {
|
||||
listen_mode = NSIGNER_LISTEN_QREXEC;
|
||||
} else if (strncmp(argv[argi + 1], "tcp:", 4) == 0) {
|
||||
listen_mode = NSIGNER_LISTEN_TCP;
|
||||
listen_target = argv[argi + 1];
|
||||
} else {
|
||||
fprintf(stderr, "Invalid --listen mode: %s (expected unix|stdio|qrexec|tcp:HOST:PORT)\n", argv[argi + 1]);
|
||||
return 1;
|
||||
}
|
||||
argi += 2;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (argi < argc && strcmp(argv[argi], "client") == 0) {
|
||||
if (listen_mode != NSIGNER_LISTEN_UNIX) {
|
||||
fprintf(stderr, "--listen is server-only; client mode uses unix abstract sockets\n");
|
||||
return 1;
|
||||
}
|
||||
return client_main(argc - argi - 1, argv + argi + 1, socket_name, socket_name_explicit);
|
||||
}
|
||||
|
||||
if (argi < argc && strcmp(argv[argi], "list") == 0) {
|
||||
if (listen_mode != NSIGNER_LISTEN_UNIX) {
|
||||
fprintf(stderr, "--listen is server-only; list inspects unix abstract sockets\n");
|
||||
return 1;
|
||||
}
|
||||
return list_sockets_main();
|
||||
}
|
||||
|
||||
@@ -1059,6 +1065,9 @@ int main(int argc, char *argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("nsigner %s\n", NSIGNER_VERSION);
|
||||
fflush(stdout);
|
||||
|
||||
mnemonic_init(&mnemonic);
|
||||
if (prompt_load_mnemonic(&mnemonic) != 0) {
|
||||
mnemonic_unload(&mnemonic);
|
||||
@@ -1091,11 +1100,20 @@ int main(int argc, char *argv[]) {
|
||||
dispatcher_init(&dispatcher, &role_table, &mnemonic, &key_store);
|
||||
|
||||
owner_uid = getuid();
|
||||
if (listen_mode == NSIGNER_LISTEN_QREXEC || listen_mode == NSIGNER_LISTEN_TCP) {
|
||||
policy_entry_t e;
|
||||
policy_table_init(&policy);
|
||||
memset(&e, 0, sizeof(e));
|
||||
strncpy(e.caller, "*", sizeof(e.caller) - 1);
|
||||
e.prompt = PROMPT_EVERY_REQUEST;
|
||||
(void)policy_table_add(&policy, &e);
|
||||
} else {
|
||||
policy_init_default(&policy, owner_uid);
|
||||
}
|
||||
|
||||
apply_test_overrides(&policy);
|
||||
|
||||
if (!socket_name_explicit) {
|
||||
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");
|
||||
crypto_wipe(&key_store);
|
||||
@@ -1106,9 +1124,27 @@ int main(int argc, char *argv[]) {
|
||||
socket_name = generated_socket_name;
|
||||
}
|
||||
|
||||
server_init(&server, socket_name, socket_name_explicit, &dispatcher, &policy);
|
||||
if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
||||
listen_target = socket_name;
|
||||
} else if (listen_mode == NSIGNER_LISTEN_TCP && socket_name_explicit) {
|
||||
fprintf(stderr, "--socket-name is only valid with unix listen mode\n");
|
||||
crypto_wipe(&key_store);
|
||||
nostr_cleanup();
|
||||
mnemonic_unload(&mnemonic);
|
||||
return 1;
|
||||
}
|
||||
|
||||
server_init(&server, listen_target, socket_name_explicit, listen_mode, &dispatcher, &policy);
|
||||
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));
|
||||
} else if (listen_mode == NSIGNER_LISTEN_TCP) {
|
||||
fprintf(stderr, "Failed to start server on %s: %s\n", listen_target, server_last_error(&server));
|
||||
} else {
|
||||
fprintf(stderr, "Failed to start server (%s): %s\n",
|
||||
(listen_mode == NSIGNER_LISTEN_QREXEC) ? "qrexec" : "stdio",
|
||||
server_last_error(&server));
|
||||
}
|
||||
crypto_wipe(&key_store);
|
||||
nostr_cleanup();
|
||||
mnemonic_unload(&mnemonic);
|
||||
@@ -1118,6 +1154,52 @@ int main(int argc, char *argv[]) {
|
||||
(void)signal(SIGINT, handle_signal);
|
||||
(void)signal(SIGTERM, handle_signal);
|
||||
|
||||
if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
||||
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||
} else if (listen_mode == NSIGNER_LISTEN_TCP) {
|
||||
printf("System is ready and waiting for connections on %s.\n", listen_target);
|
||||
} else if (listen_mode == NSIGNER_LISTEN_QREXEC) {
|
||||
printf("System is ready and waiting for a qrexec request.\n");
|
||||
} else {
|
||||
printf("System is ready and waiting for a stdio request.\n");
|
||||
}
|
||||
fflush(stdout);
|
||||
|
||||
if (listen_mode == NSIGNER_LISTEN_STDIO || listen_mode == NSIGNER_LISTEN_QREXEC) {
|
||||
int hrc = server_handle_one(&server, NULL, NULL);
|
||||
server_stop(&server);
|
||||
crypto_wipe(&key_store);
|
||||
nostr_cleanup();
|
||||
mnemonic_unload(&mnemonic);
|
||||
return (hrc < 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
if (listen_mode == NSIGNER_LISTEN_TCP) {
|
||||
pfds[0].fd = server.listen_fd;
|
||||
pfds[0].events = POLLIN;
|
||||
|
||||
while (g_running && server.running) {
|
||||
int prc = poll(pfds, 1, 200);
|
||||
if (prc < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (prc > 0 && (pfds[0].revents & POLLIN)) {
|
||||
if (server_handle_one(&server, tcp_activity_stdout_cb, NULL) < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server_stop(&server);
|
||||
crypto_wipe(&key_store);
|
||||
nostr_cleanup();
|
||||
mnemonic_unload(&mnemonic);
|
||||
return 0;
|
||||
}
|
||||
|
||||
memset(&g_activity_log, 0, sizeof(g_activity_log));
|
||||
g_auto_approve = 0;
|
||||
server_set_prompt_always_allow(0);
|
||||
@@ -1149,7 +1231,7 @@ int main(int argc, char *argv[]) {
|
||||
char ch = '\0';
|
||||
if (read(STDIN_FILENO, &ch, 1) > 0) {
|
||||
ch = (char)tolower((unsigned char)ch);
|
||||
if (ch == 'q') {
|
||||
if (ch == 'q' || ch == 'x') {
|
||||
g_running = 0;
|
||||
} else if (ch == 'r') {
|
||||
render_status(&role_table, &mnemonic, derived_count, socket_name);
|
||||
|
||||
497
src/server.c
497
src/server.c
@@ -373,12 +373,19 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
|
||||
#define SERVER_SOCKET_NAME_MAX 108
|
||||
#define SERVER_MAX_MSG_SIZE 65536
|
||||
|
||||
#define NSIGNER_LISTEN_UNIX 0
|
||||
#define NSIGNER_LISTEN_STDIO 1
|
||||
#define NSIGNER_LISTEN_QREXEC 2
|
||||
#define NSIGNER_LISTEN_TCP 3
|
||||
|
||||
/* Caller identity */
|
||||
typedef struct {
|
||||
uid_t uid;
|
||||
gid_t gid;
|
||||
pid_t pid;
|
||||
char caller_id[64]; /* "uid:<n>" */
|
||||
int kind;
|
||||
char caller_id[64]; /* "uid:<n>" or "qubes:<vm>" */
|
||||
char source_qube[64];
|
||||
} caller_identity_t;
|
||||
|
||||
/* Server context */
|
||||
@@ -387,6 +394,8 @@ typedef struct {
|
||||
char last_error[256];
|
||||
int listen_fd;
|
||||
int running;
|
||||
int listen_mode;
|
||||
int stdio_handled;
|
||||
dispatcher_ctx_t *dispatcher;
|
||||
policy_table_t *policy;
|
||||
int socket_name_explicit;
|
||||
@@ -395,6 +404,7 @@ typedef struct {
|
||||
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
|
||||
* socket_name_explicit should be non-zero when provided via --socket-name override. */
|
||||
void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit,
|
||||
int listen_mode,
|
||||
dispatcher_ctx_t *dispatcher, policy_table_t *policy);
|
||||
|
||||
/* Start listening. Returns 0 on success, -1 on error. */
|
||||
@@ -442,6 +452,9 @@ int socket_name_random(char *out, size_t out_len);
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
|
||||
int transport_send_framed(int fd, const char *payload);
|
||||
int transport_recv_framed(int fd, char **out_payload, size_t max_size);
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
@@ -456,6 +469,157 @@ int socket_name_random(char *out, size_t out_len);
|
||||
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;
|
||||
}
|
||||
@@ -488,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");
|
||||
@@ -527,98 +705,81 @@ static void server_set_error(server_ctx_t *ctx, const char *msg) {
|
||||
ctx->last_error[sizeof(ctx->last_error) - 1] = '\0';
|
||||
}
|
||||
|
||||
static int read_full(int fd, void *buf, size_t len) {
|
||||
unsigned char *p = (unsigned char *)buf;
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = read(fd, p + off, len - off);
|
||||
if (n == 0) {
|
||||
return -1;
|
||||
}
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
static int parse_tcp_target(const char *target,
|
||||
int *out_family,
|
||||
char *out_host,
|
||||
size_t out_host_sz,
|
||||
uint16_t *out_port) {
|
||||
const char *p;
|
||||
const char *host_start;
|
||||
const char *host_end;
|
||||
const char *port_start;
|
||||
char port_buf[16];
|
||||
size_t host_len;
|
||||
size_t port_len;
|
||||
char *endptr = NULL;
|
||||
long port_long;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int write_full(int fd, const void *buf, size_t len) {
|
||||
const unsigned char *p = (const unsigned char *)buf;
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = write(fd, p + off, len - off);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int recv_framed(int fd, char **out_payload) {
|
||||
uint32_t be_len;
|
||||
uint32_t len;
|
||||
char *payload;
|
||||
|
||||
if (out_payload == NULL) {
|
||||
if (target == NULL || out_family == NULL || out_host == NULL || out_port == NULL ||
|
||||
out_host_sz == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_payload = NULL;
|
||||
|
||||
if (read_full(fd, &be_len, sizeof(be_len)) != 0) {
|
||||
if (strncmp(target, "tcp:", 4) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = ntohl(be_len);
|
||||
if (len == 0 || len > SERVER_MAX_MSG_SIZE) {
|
||||
p = target + 4;
|
||||
if (*p == '[') {
|
||||
host_start = p + 1;
|
||||
host_end = strchr(host_start, ']');
|
||||
if (host_end == NULL || host_end[1] != ':') {
|
||||
return -1;
|
||||
}
|
||||
port_start = host_end + 2;
|
||||
} else {
|
||||
host_start = p;
|
||||
host_end = strrchr(p, ':');
|
||||
if (host_end == NULL || host_end == host_start) {
|
||||
return -1;
|
||||
}
|
||||
port_start = host_end + 1;
|
||||
}
|
||||
|
||||
host_len = (size_t)(host_end - host_start);
|
||||
if (host_len == 0 || host_len >= out_host_sz) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(out_host, host_start, host_len);
|
||||
out_host[host_len] = '\0';
|
||||
|
||||
port_len = strlen(port_start);
|
||||
if (port_len == 0 || port_len >= sizeof(port_buf)) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(port_buf, port_start, port_len + 1);
|
||||
|
||||
errno = 0;
|
||||
port_long = strtol(port_buf, &endptr, 10);
|
||||
if (errno != 0 || endptr == port_buf || *endptr != '\0' || port_long < 1 || port_long > 65535) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload = (char *)malloc((size_t)len + 1U);
|
||||
if (payload == NULL) {
|
||||
{
|
||||
struct in6_addr addr6;
|
||||
struct in_addr addr4;
|
||||
|
||||
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 (read_full(fd, payload, len) != 0) {
|
||||
free(payload);
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload[len] = '\0';
|
||||
*out_payload = payload;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_framed(int fd, const char *payload) {
|
||||
uint32_t len;
|
||||
uint32_t be_len;
|
||||
|
||||
if (payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = (uint32_t)strlen(payload);
|
||||
be_len = htonl(len);
|
||||
|
||||
if (write_full(fd, &be_len, sizeof(be_len)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (write_full(fd, payload, len) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_port = (uint16_t)port_long;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -697,6 +858,7 @@ static int extract_method_and_selector(const char *json,
|
||||
}
|
||||
|
||||
void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit,
|
||||
int listen_mode,
|
||||
dispatcher_ctx_t *dispatcher, policy_table_t *policy) {
|
||||
if (ctx == NULL) {
|
||||
return;
|
||||
@@ -709,6 +871,8 @@ void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_exp
|
||||
ctx->socket_name[sizeof(ctx->socket_name) - 1] = '\0';
|
||||
}
|
||||
ctx->listen_fd = -1;
|
||||
ctx->listen_mode = listen_mode;
|
||||
ctx->stdio_handled = 0;
|
||||
ctx->dispatcher = dispatcher;
|
||||
ctx->policy = policy;
|
||||
ctx->socket_name_explicit = socket_name_explicit ? 1 : 0;
|
||||
@@ -732,6 +896,105 @@ int server_start(server_ctx_t *ctx) {
|
||||
|
||||
server_set_error(ctx, NULL);
|
||||
|
||||
if (ctx->listen_mode == NSIGNER_LISTEN_STDIO || ctx->listen_mode == NSIGNER_LISTEN_QREXEC) {
|
||||
ctx->listen_fd = STDIN_FILENO;
|
||||
ctx->running = 1;
|
||||
ctx->stdio_handled = 0;
|
||||
server_set_error(ctx, NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ctx->listen_mode == NSIGNER_LISTEN_TCP) {
|
||||
int family;
|
||||
uint16_t port;
|
||||
char host[64];
|
||||
int one = 1;
|
||||
|
||||
int prc = parse_tcp_target(ctx->socket_name, &family, host, sizeof(host), &port);
|
||||
if (prc != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"invalid tcp listen target: %s (expected tcp:IPv4:PORT or tcp:[IPv6]:PORT)",
|
||||
ctx->socket_name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
fd = socket(family, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"socket(tcp) failed: %s",
|
||||
strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
|
||||
if (family == AF_INET) {
|
||||
struct sockaddr_in addr4;
|
||||
memset(&addr4, 0, sizeof(addr4));
|
||||
addr4.sin_family = AF_INET;
|
||||
addr4.sin_port = htons(port);
|
||||
if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) {
|
||||
close(fd);
|
||||
server_set_error(ctx, "inet_pton(AF_INET) failed for listen target");
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(%s) failed: %s",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
struct sockaddr_in6 addr6;
|
||||
memset(&addr6, 0, sizeof(addr6));
|
||||
addr6.sin6_family = AF_INET6;
|
||||
addr6.sin6_port = htons(port);
|
||||
if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) {
|
||||
close(fd);
|
||||
server_set_error(ctx, "inet_pton(AF_INET6) failed for listen target");
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(%s) failed: %s",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (listen(fd, 16) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"listen() failed: %s",
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"fcntl(O_NONBLOCK) failed: %s",
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx->listen_fd = fd;
|
||||
ctx->running = 1;
|
||||
server_set_error(ctx, NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
@@ -835,6 +1098,56 @@ int server_get_caller(int fd, caller_identity_t *out) {
|
||||
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
if (fd == STDIN_FILENO) {
|
||||
const char *src = getenv("QREXEC_REMOTE_DOMAIN");
|
||||
out->kind = NSIGNER_LISTEN_STDIO;
|
||||
if (src != NULL && src[0] != '\0') {
|
||||
out->kind = NSIGNER_LISTEN_QREXEC;
|
||||
strncpy(out->source_qube, src, sizeof(out->source_qube) - 1);
|
||||
out->source_qube[sizeof(out->source_qube) - 1] = '\0';
|
||||
(void)snprintf(out->caller_id, sizeof(out->caller_id), "qubes:%.57s", out->source_qube);
|
||||
return 0;
|
||||
}
|
||||
|
||||
out->uid = getuid();
|
||||
out->gid = getgid();
|
||||
out->pid = getpid();
|
||||
(void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
{
|
||||
struct sockaddr_storage peer;
|
||||
socklen_t peer_len = sizeof(peer);
|
||||
if (getpeername(fd, (struct sockaddr *)&peer, &peer_len) == 0) {
|
||||
if (peer.ss_family == AF_INET) {
|
||||
const struct sockaddr_in *in4 = (const struct sockaddr_in *)&peer;
|
||||
char ip[INET_ADDRSTRLEN];
|
||||
if (inet_ntop(AF_INET, &in4->sin_addr, ip, sizeof(ip)) != NULL) {
|
||||
out->kind = NSIGNER_LISTEN_TCP;
|
||||
(void)snprintf(out->caller_id,
|
||||
sizeof(out->caller_id),
|
||||
"tcp:%s:%u",
|
||||
ip,
|
||||
(unsigned int)ntohs(in4->sin_port));
|
||||
return 0;
|
||||
}
|
||||
} else if (peer.ss_family == AF_INET6) {
|
||||
const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)&peer;
|
||||
char ip6[INET6_ADDRSTRLEN];
|
||||
if (inet_ntop(AF_INET6, &in6->sin6_addr, ip6, sizeof(ip6)) != NULL) {
|
||||
out->kind = NSIGNER_LISTEN_TCP;
|
||||
(void)snprintf(out->caller_id,
|
||||
sizeof(out->caller_id),
|
||||
"tcp:[%s]:%u",
|
||||
ip6,
|
||||
(unsigned int)ntohs(in6->sin6_port));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
|
||||
return -1;
|
||||
}
|
||||
@@ -842,6 +1155,7 @@ int server_get_caller(int fd, caller_identity_t *out) {
|
||||
out->uid = cred.uid;
|
||||
out->gid = cred.gid;
|
||||
out->pid = cred.pid;
|
||||
out->kind = NSIGNER_LISTEN_UNIX;
|
||||
(void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid);
|
||||
return 0;
|
||||
}
|
||||
@@ -864,6 +1178,13 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ctx->listen_mode == NSIGNER_LISTEN_STDIO || ctx->listen_mode == NSIGNER_LISTEN_QREXEC) {
|
||||
if (ctx->stdio_handled) {
|
||||
return 0;
|
||||
}
|
||||
client_fd = STDIN_FILENO;
|
||||
ctx->stdio_handled = 1;
|
||||
} else {
|
||||
client_fd = accept(ctx->listen_fd, NULL, NULL);
|
||||
if (client_fd < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
@@ -871,19 +1192,24 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (server_get_caller(client_fd, &caller) != 0) {
|
||||
if (client_fd != STDIN_FILENO) {
|
||||
close(client_fd);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (recv_framed(client_fd, &request) != 0) {
|
||||
if (transport_recv_framed(client_fd, &request, SERVER_MAX_MSG_SIZE) != 0) {
|
||||
response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32700,\"message\":\"parse_error\"}}");
|
||||
if (response != NULL) {
|
||||
(void)send_framed(client_fd, response);
|
||||
(void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response);
|
||||
free(response);
|
||||
}
|
||||
if (client_fd != STDIN_FILENO) {
|
||||
close(client_fd);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -917,14 +1243,13 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
||||
}
|
||||
|
||||
if (response != NULL) {
|
||||
(void)send_framed(client_fd, response);
|
||||
(void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response);
|
||||
}
|
||||
|
||||
(void)snprintf(activity,
|
||||
sizeof(activity),
|
||||
"uid=%u pid=%d %s(%s) %s",
|
||||
(unsigned int)caller.uid,
|
||||
(int)caller.pid,
|
||||
"%s %s(%s) %s",
|
||||
caller.caller_id,
|
||||
method,
|
||||
role_name,
|
||||
verdict);
|
||||
@@ -935,7 +1260,9 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
||||
|
||||
free(request);
|
||||
free(response);
|
||||
if (client_fd != STDIN_FILENO) {
|
||||
close(client_fd);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -945,7 +1272,9 @@ void server_stop(server_ctx_t *ctx) {
|
||||
}
|
||||
|
||||
if (ctx->listen_fd >= 0) {
|
||||
if (ctx->listen_mode == NSIGNER_LISTEN_UNIX || ctx->listen_mode == NSIGNER_LISTEN_TCP) {
|
||||
close(ctx->listen_fd);
|
||||
}
|
||||
ctx->listen_fd = -1;
|
||||
}
|
||||
ctx->running = 0;
|
||||
|
||||
104
src/transport_frame.c
Normal file
104
src/transport_frame.c
Normal file
@@ -0,0 +1,104 @@
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int transport_read_full(int fd, void *buf, size_t len) {
|
||||
unsigned char *p = (unsigned char *)buf;
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = read(fd, p + off, len - off);
|
||||
if (n == 0) {
|
||||
return -1;
|
||||
}
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int transport_write_full(int fd, const void *buf, size_t len) {
|
||||
const unsigned char *p = (const unsigned char *)buf;
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = write(fd, p + off, len - off);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int transport_send_framed(int fd, const char *payload) {
|
||||
uint32_t len;
|
||||
uint32_t be_len;
|
||||
|
||||
if (payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = (uint32_t)strlen(payload);
|
||||
be_len = htonl(len);
|
||||
|
||||
if (transport_write_full(fd, &be_len, sizeof(be_len)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (transport_write_full(fd, payload, len) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int transport_recv_framed(int fd, char **out_payload, size_t max_size) {
|
||||
uint32_t be_len;
|
||||
uint32_t len;
|
||||
char *payload;
|
||||
|
||||
if (out_payload == NULL || max_size == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_payload = NULL;
|
||||
|
||||
if (transport_read_full(fd, &be_len, sizeof(be_len)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = ntohl(be_len);
|
||||
if (len == 0 || len > max_size) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload = (char *)malloc((size_t)len + 1U);
|
||||
if (payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (transport_read_full(fd, payload, len) != 0) {
|
||||
free(payload);
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload[len] = '\0';
|
||||
*out_payload = payload;
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user