v0.0.6 - Tier-1 TCP listener + FIPS deployment documentation
This commit is contained in:
@@ -67,6 +67,7 @@ RUN gcc -static -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -s -Wa
|
|||||||
/build/src/dispatcher.c \
|
/build/src/dispatcher.c \
|
||||||
/build/src/policy.c \
|
/build/src/policy.c \
|
||||||
/build/src/server.c \
|
/build/src/server.c \
|
||||||
|
/build/src/transport_frame.c \
|
||||||
/build/src/key_store.c \
|
/build/src/key_store.c \
|
||||||
/build/src/socket_name.c \
|
/build/src/socket_name.c \
|
||||||
/build/nostr_core_lib/libnostr_core_x64.a \
|
/build/nostr_core_lib/libnostr_core_x64.a \
|
||||||
|
|||||||
1
Makefile
1
Makefile
@@ -18,6 +18,7 @@ SOURCES := \
|
|||||||
$(SRC_DIR)/dispatcher.c \
|
$(SRC_DIR)/dispatcher.c \
|
||||||
$(SRC_DIR)/policy.c \
|
$(SRC_DIR)/policy.c \
|
||||||
$(SRC_DIR)/server.c \
|
$(SRC_DIR)/server.c \
|
||||||
|
$(SRC_DIR)/transport_frame.c \
|
||||||
$(SRC_DIR)/key_store.c \
|
$(SRC_DIR)/key_store.c \
|
||||||
$(SRC_DIR)/socket_name.c
|
$(SRC_DIR)/socket_name.c
|
||||||
|
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -237,6 +237,9 @@ Naming rules:
|
|||||||
Discovery:
|
Discovery:
|
||||||
|
|
||||||
- `nsigner list` enumerates currently bound `nsigner_*` abstract sockets by reading `/proc/net/unix`.
|
- `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.
|
||||||
|
|
||||||
### 7.2 ESP32 MCU: USB-CDC serial
|
### 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
|
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 loopback transport mode (no TUI; serves requests until terminated):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nsigner --listen tcp:127.0.0.1:8080
|
||||||
|
```
|
||||||
|
|
||||||
### 9.2 Send a request (client mode)
|
### 9.2 Send a request (client mode)
|
||||||
|
|
||||||
From another terminal, target the signer by its socket name:
|
From another terminal, target the signer by its socket name:
|
||||||
@@ -365,6 +386,9 @@ Static build:
|
|||||||
## 11. Document map
|
## 11. Document map
|
||||||
|
|
||||||
- [`README.md`](README.md): authoritative behavior specification for the foreground single-program model
|
- [`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/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
|
- [`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
|
- [`firmware/README.md`](firmware/README.md): firmware-side notes for MCU transport/UI integration
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ It is written for agent/tool authors implementing robust request flows against t
|
|||||||
|
|
||||||
## 2. Discovery and socket targeting
|
## 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 `@`.
|
- Socket names are exposed in `/proc/net/unix` with a leading `@`.
|
||||||
- Typical runtime names: `@nsigner_hairy_dog`, `@nsigner_brave_canyon`.
|
- 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.
|
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
|
## 3. Transport framing
|
||||||
185
documents/FIPS_DEPLOYMENT.md
Normal file
185
documents/FIPS_DEPLOYMENT.md
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# FIPS_DEPLOYMENT.md
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
Tier-1 objective:
|
||||||
|
|
||||||
|
- Keep `nsigner` transport simple (`--listen tcp:127.0.0.1:PORT` or `--listen tcp:[::1]: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.
|
||||||
|
- Automatic caller->npub enrichment from FIPS session metadata.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architecture
|
||||||
|
|
||||||
|
Two cooperating layers:
|
||||||
|
|
||||||
|
1. **Signer process layer** (`nsigner`)
|
||||||
|
- Listens on loopback TCP only.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
Conceptually:
|
||||||
|
|
||||||
|
`client app -> local FIPS endpoint -> FIPS mesh -> remote FIPS endpoint -> 127.0.0.1:PORT -> 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 loopback TCP listen mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build/nsigner --listen tcp:127.0.0.1:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Or IPv6 loopback:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build/nsigner --listen tcp:[::1]:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior notes:
|
||||||
|
|
||||||
|
- Non-loopback values are rejected by design.
|
||||||
|
- 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 loopback in signer environment.
|
||||||
|
2. Configure FIPS service/forwarding so remote authenticated peer traffic is delivered to that loopback 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.
|
||||||
|
- `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
|
||||||
|
|
||||||
|
- Keep listener loopback-only in Tier-1.
|
||||||
|
- 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:[::1]:PORT`.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
- Use explicit loopback host and valid numeric port.
|
||||||
|
|
||||||
|
### 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`)
|
||||||
|
|
||||||
|
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.4 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))
|
||||||
|
```
|
||||||
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.
|
- Expand integration coverage for NIP-04/NIP-44 edge cases and negative-path errors.
|
||||||
- Document and test static artifact size budgets across targets.
|
- Document and test static artifact size budgets across targets.
|
||||||
- Define MCU transport adapter contract to prepare desktop/firmware parity.
|
- 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 loopback 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.
|
||||||
|
- `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.
|
||||||
|
- 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] 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 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.
|
||||||
|
- 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 loopback 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?
|
||||||
|
|||||||
252
src/main.c
252
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_SOCKET_NAME_MAX 108
|
||||||
#define SERVER_MAX_MSG_SIZE 65536
|
#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 */
|
/* Caller identity */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uid_t uid;
|
uid_t uid;
|
||||||
gid_t gid;
|
gid_t gid;
|
||||||
pid_t pid;
|
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;
|
} caller_identity_t;
|
||||||
|
|
||||||
/* Server context */
|
/* Server context */
|
||||||
@@ -387,6 +394,8 @@ typedef struct {
|
|||||||
char last_error[256];
|
char last_error[256];
|
||||||
int listen_fd;
|
int listen_fd;
|
||||||
int running;
|
int running;
|
||||||
|
int listen_mode;
|
||||||
|
int stdio_handled;
|
||||||
dispatcher_ctx_t *dispatcher;
|
dispatcher_ctx_t *dispatcher;
|
||||||
policy_table_t *policy;
|
policy_table_t *policy;
|
||||||
int socket_name_explicit;
|
int socket_name_explicit;
|
||||||
@@ -395,6 +404,7 @@ typedef struct {
|
|||||||
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
|
/* 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. */
|
* 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,
|
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);
|
dispatcher_ctx_t *dispatcher, policy_table_t *policy);
|
||||||
|
|
||||||
/* Start listening. Returns 0 on success, -1 on error. */
|
/* 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) */
|
/* Version information (auto-updated by build/version tooling) */
|
||||||
#define NSIGNER_VERSION_MAJOR 0
|
#define NSIGNER_VERSION_MAJOR 0
|
||||||
#define NSIGNER_VERSION_MINOR 0
|
#define NSIGNER_VERSION_MINOR 0
|
||||||
#define NSIGNER_VERSION_PATCH 5
|
#define NSIGNER_VERSION_PATCH 6
|
||||||
#define NSIGNER_VERSION "v0.0.5"
|
#define NSIGNER_VERSION "v0.0.6"
|
||||||
|
|
||||||
|
|
||||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
/* 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 <nostr_core/nostr_common.h>
|
||||||
|
|
||||||
#include <arpa/inet.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/types.h>
|
||||||
#include <sys/un.h>
|
#include <sys/un.h>
|
||||||
#include <termios.h>
|
#include <termios.h>
|
||||||
|
#include <time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#define NSIGNER_DEFAULT_SOCKET_NAME "nsigner"
|
#define NSIGNER_DEFAULT_SOCKET_NAME "nsigner"
|
||||||
@@ -501,100 +515,6 @@ static int read_line_stdin(char *buf, size_t buf_sz) {
|
|||||||
return 0;
|
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) {
|
static int connect_abstract_socket(const char *name) {
|
||||||
int fd;
|
int fd;
|
||||||
@@ -628,7 +548,8 @@ static int connect_abstract_socket(const char *name) {
|
|||||||
static void print_usage(const char *program_name) {
|
static void print_usage(const char *program_name) {
|
||||||
printf("nsigner - single-binary signer program\n");
|
printf("nsigner - single-binary signer program\n");
|
||||||
printf("Usage:\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 '<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 [--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);
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (send_framed(fd, request) != 0) {
|
if (transport_send_framed(fd, request) != 0) {
|
||||||
perror("send");
|
perror("send");
|
||||||
close(fd);
|
close(fd);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recv_framed(fd, &response) != 0) {
|
if (transport_recv_framed(fd, &response, SERVER_MAX_MSG_SIZE) != 0) {
|
||||||
perror("recv");
|
perror("recv");
|
||||||
close(fd);
|
close(fd);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -799,15 +720,29 @@ 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) {
|
static void activity_log_cb(const char *message, void *user_data) {
|
||||||
int idx;
|
int idx;
|
||||||
|
time_t now;
|
||||||
|
struct tm tm_now;
|
||||||
|
char ts[16];
|
||||||
|
|
||||||
(void)user_data;
|
(void)user_data;
|
||||||
if (message == NULL) {
|
if (message == NULL) {
|
||||||
return;
|
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;
|
idx = g_activity_log.count % ACTIVITY_LOG_CAP;
|
||||||
strncpy(g_activity_log.lines[idx], message, sizeof(g_activity_log.lines[idx]) - 1);
|
(void)snprintf(g_activity_log.lines[idx],
|
||||||
g_activity_log.lines[idx][sizeof(g_activity_log.lines[idx]) - 1] = '\0';
|
sizeof(g_activity_log.lines[idx]),
|
||||||
|
"%s %s",
|
||||||
|
ts,
|
||||||
|
message);
|
||||||
g_activity_log.count++;
|
g_activity_log.count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,7 +813,7 @@ static int setup_default_role(role_table_t *role_table) {
|
|||||||
static int prompt_load_mnemonic(mnemonic_state_t *mnemonic) {
|
static int prompt_load_mnemonic(mnemonic_state_t *mnemonic) {
|
||||||
char phrase[MNEMONIC_MAX_LEN];
|
char phrase[MNEMONIC_MAX_LEN];
|
||||||
char phrase_copy[MNEMONIC_MAX_LEN];
|
char phrase_copy[MNEMONIC_MAX_LEN];
|
||||||
char mode[16];
|
char mode[MNEMONIC_MAX_LEN];
|
||||||
struct termios old_term;
|
struct termios old_term;
|
||||||
struct termios new_term;
|
struct termios new_term;
|
||||||
int have_term = 0;
|
int have_term = 0;
|
||||||
@@ -887,13 +822,21 @@ static int prompt_load_mnemonic(mnemonic_state_t *mnemonic) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("Mnemonic source: [E]nter existing or [G]enerate new (default E): ");
|
printf("Mnemonic source: [E]nter existing or [G]enerate new (default E; you can also paste mnemonic here): ");
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
if (read_line_stdin(mode, sizeof(mode)) != 0) {
|
if (read_line_stdin(mode, sizeof(mode)) != 0) {
|
||||||
fprintf(stderr, "Failed to read mnemonic source choice\n");
|
fprintf(stderr, "Failed to read mnemonic source choice\n");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strchr(mode, ' ') != NULL && mode[0] != 'g' && mode[0] != 'G') {
|
||||||
|
if (mnemonic_load(mnemonic, mode) != 0) {
|
||||||
|
fprintf(stderr, "Invalid mnemonic (must be 12/15/18/21/24 words)\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (mode[0] == 'g' || mode[0] == 'G') {
|
if (mode[0] == 'g' || mode[0] == 'G') {
|
||||||
int idx = 1;
|
int idx = 1;
|
||||||
char *ctx = NULL;
|
char *ctx = NULL;
|
||||||
@@ -1017,6 +960,8 @@ int main(int argc, char *argv[]) {
|
|||||||
const char *socket_name = NSIGNER_DEFAULT_SOCKET_NAME;
|
const char *socket_name = NSIGNER_DEFAULT_SOCKET_NAME;
|
||||||
char generated_socket_name[SERVER_SOCKET_NAME_MAX];
|
char generated_socket_name[SERVER_SOCKET_NAME_MAX];
|
||||||
int socket_name_explicit = 0;
|
int socket_name_explicit = 0;
|
||||||
|
int listen_mode = NSIGNER_LISTEN_UNIX;
|
||||||
|
const char *listen_target = NSIGNER_DEFAULT_SOCKET_NAME;
|
||||||
int argi = 1;
|
int argi = 1;
|
||||||
|
|
||||||
while (argi < argc) {
|
while (argi < argc) {
|
||||||
@@ -1032,14 +977,43 @@ int main(int argc, char *argv[]) {
|
|||||||
argi += 2;
|
argi += 2;
|
||||||
continue;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (argi < argc && strcmp(argv[argi], "client") == 0) {
|
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);
|
return client_main(argc - argi - 1, argv + argi + 1, socket_name, socket_name_explicit);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (argi < argc && strcmp(argv[argi], "list") == 0) {
|
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();
|
return list_sockets_main();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1091,11 +1065,20 @@ int main(int argc, char *argv[]) {
|
|||||||
dispatcher_init(&dispatcher, &role_table, &mnemonic, &key_store);
|
dispatcher_init(&dispatcher, &role_table, &mnemonic, &key_store);
|
||||||
|
|
||||||
owner_uid = getuid();
|
owner_uid = getuid();
|
||||||
policy_init_default(&policy, owner_uid);
|
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);
|
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) {
|
if (socket_name_random(generated_socket_name, sizeof(generated_socket_name)) != 0) {
|
||||||
fprintf(stderr, "Failed to generate random socket name\n");
|
fprintf(stderr, "Failed to generate random socket name\n");
|
||||||
crypto_wipe(&key_store);
|
crypto_wipe(&key_store);
|
||||||
@@ -1106,9 +1089,27 @@ int main(int argc, char *argv[]) {
|
|||||||
socket_name = generated_socket_name;
|
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 (server_start(&server) != 0) {
|
||||||
fprintf(stderr, "Failed to start server on @%s: %s\n", socket_name, server_last_error(&server));
|
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);
|
crypto_wipe(&key_store);
|
||||||
nostr_cleanup();
|
nostr_cleanup();
|
||||||
mnemonic_unload(&mnemonic);
|
mnemonic_unload(&mnemonic);
|
||||||
@@ -1118,6 +1119,41 @@ int main(int argc, char *argv[]) {
|
|||||||
(void)signal(SIGINT, handle_signal);
|
(void)signal(SIGINT, handle_signal);
|
||||||
(void)signal(SIGTERM, handle_signal);
|
(void)signal(SIGTERM, handle_signal);
|
||||||
|
|
||||||
|
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, NULL, 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));
|
memset(&g_activity_log, 0, sizeof(g_activity_log));
|
||||||
g_auto_approve = 0;
|
g_auto_approve = 0;
|
||||||
server_set_prompt_always_allow(0);
|
server_set_prompt_always_allow(0);
|
||||||
|
|||||||
359
src/server.c
359
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_SOCKET_NAME_MAX 108
|
||||||
#define SERVER_MAX_MSG_SIZE 65536
|
#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 */
|
/* Caller identity */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uid_t uid;
|
uid_t uid;
|
||||||
gid_t gid;
|
gid_t gid;
|
||||||
pid_t pid;
|
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;
|
} caller_identity_t;
|
||||||
|
|
||||||
/* Server context */
|
/* Server context */
|
||||||
@@ -387,6 +394,8 @@ typedef struct {
|
|||||||
char last_error[256];
|
char last_error[256];
|
||||||
int listen_fd;
|
int listen_fd;
|
||||||
int running;
|
int running;
|
||||||
|
int listen_mode;
|
||||||
|
int stdio_handled;
|
||||||
dispatcher_ctx_t *dispatcher;
|
dispatcher_ctx_t *dispatcher;
|
||||||
policy_table_t *policy;
|
policy_table_t *policy;
|
||||||
int socket_name_explicit;
|
int socket_name_explicit;
|
||||||
@@ -395,6 +404,7 @@ typedef struct {
|
|||||||
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
|
/* 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. */
|
* 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,
|
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);
|
dispatcher_ctx_t *dispatcher, policy_table_t *policy);
|
||||||
|
|
||||||
/* Start listening. Returns 0 on success, -1 on error. */
|
/* 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 */
|
/* 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 <arpa/inet.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
@@ -527,98 +540,81 @@ static void server_set_error(server_ctx_t *ctx, const char *msg) {
|
|||||||
ctx->last_error[sizeof(ctx->last_error) - 1] = '\0';
|
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) {
|
static int parse_tcp_target(const char *target,
|
||||||
ssize_t n = read(fd, p + off, len - off);
|
int *out_family,
|
||||||
if (n == 0) {
|
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;
|
||||||
|
|
||||||
|
if (target == NULL || out_family == NULL || out_host == NULL || out_port == NULL ||
|
||||||
|
out_host_sz == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strncmp(target, "tcp:", 4) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
p = target + 4;
|
||||||
|
if (*p == '[') {
|
||||||
|
host_start = p + 1;
|
||||||
|
host_end = strchr(host_start, ']');
|
||||||
|
if (host_end == NULL || host_end[1] != ':') {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (n < 0) {
|
port_start = host_end + 2;
|
||||||
if (errno == EINTR) {
|
} else {
|
||||||
continue;
|
host_start = p;
|
||||||
}
|
host_end = strrchr(p, ':');
|
||||||
|
if (host_end == NULL || host_end == host_start) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
off += (size_t)n;
|
port_start = host_end + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
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';
|
||||||
|
|
||||||
static int write_full(int fd, const void *buf, size_t len) {
|
port_len = strlen(port_start);
|
||||||
const unsigned char *p = (const unsigned char *)buf;
|
if (port_len == 0 || port_len >= sizeof(port_buf)) {
|
||||||
size_t off = 0;
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(port_buf, port_start, port_len + 1);
|
||||||
|
|
||||||
while (off < len) {
|
errno = 0;
|
||||||
ssize_t n = write(fd, p + off, len - off);
|
port_long = strtol(port_buf, &endptr, 10);
|
||||||
if (n < 0) {
|
if (errno != 0 || endptr == port_buf || *endptr != '\0' || port_long < 1 || port_long > 65535) {
|
||||||
if (errno == EINTR) {
|
return -1;
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
if (strcmp(out_host, "::1") == 0) {
|
||||||
|
*out_family = AF_INET6;
|
||||||
|
} else {
|
||||||
|
struct in_addr addr4;
|
||||||
|
if (inet_pton(AF_INET, out_host, &addr4) != 1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
off += (size_t)n;
|
if ((ntohl(addr4.s_addr) & 0xff000000U) != 0x7f000000U) {
|
||||||
}
|
return -2;
|
||||||
|
}
|
||||||
return 0;
|
*out_family = AF_INET;
|
||||||
}
|
|
||||||
|
|
||||||
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 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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,6 +693,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,
|
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) {
|
dispatcher_ctx_t *dispatcher, policy_table_t *policy) {
|
||||||
if (ctx == NULL) {
|
if (ctx == NULL) {
|
||||||
return;
|
return;
|
||||||
@@ -709,6 +706,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->socket_name[sizeof(ctx->socket_name) - 1] = '\0';
|
||||||
}
|
}
|
||||||
ctx->listen_fd = -1;
|
ctx->listen_fd = -1;
|
||||||
|
ctx->listen_mode = listen_mode;
|
||||||
|
ctx->stdio_handled = 0;
|
||||||
ctx->dispatcher = dispatcher;
|
ctx->dispatcher = dispatcher;
|
||||||
ctx->policy = policy;
|
ctx->policy = policy;
|
||||||
ctx->socket_name_explicit = socket_name_explicit ? 1 : 0;
|
ctx->socket_name_explicit = socket_name_explicit ? 1 : 0;
|
||||||
@@ -732,6 +731,112 @@ int server_start(server_ctx_t *ctx) {
|
|||||||
|
|
||||||
server_set_error(ctx, NULL);
|
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 == -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)",
|
||||||
|
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);
|
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
(void)snprintf(ctx->last_error,
|
(void)snprintf(ctx->last_error,
|
||||||
@@ -835,6 +940,56 @@ int server_get_caller(int fd, caller_identity_t *out) {
|
|||||||
|
|
||||||
memset(out, 0, sizeof(*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) {
|
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -842,6 +997,7 @@ int server_get_caller(int fd, caller_identity_t *out) {
|
|||||||
out->uid = cred.uid;
|
out->uid = cred.uid;
|
||||||
out->gid = cred.gid;
|
out->gid = cred.gid;
|
||||||
out->pid = cred.pid;
|
out->pid = cred.pid;
|
||||||
|
out->kind = NSIGNER_LISTEN_UNIX;
|
||||||
(void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid);
|
(void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -864,26 +1020,38 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
client_fd = accept(ctx->listen_fd, NULL, NULL);
|
if (ctx->listen_mode == NSIGNER_LISTEN_STDIO || ctx->listen_mode == NSIGNER_LISTEN_QREXEC) {
|
||||||
if (client_fd < 0) {
|
if (ctx->stdio_handled) {
|
||||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return -1;
|
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) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server_get_caller(client_fd, &caller) != 0) {
|
if (server_get_caller(client_fd, &caller) != 0) {
|
||||||
close(client_fd);
|
if (client_fd != STDIN_FILENO) {
|
||||||
|
close(client_fd);
|
||||||
|
}
|
||||||
return -1;
|
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\"}}");
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32700,\"message\":\"parse_error\"}}");
|
||||||
if (response != NULL) {
|
if (response != NULL) {
|
||||||
(void)send_framed(client_fd, response);
|
(void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response);
|
||||||
free(response);
|
free(response);
|
||||||
}
|
}
|
||||||
close(client_fd);
|
if (client_fd != STDIN_FILENO) {
|
||||||
|
close(client_fd);
|
||||||
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,14 +1085,13 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response != NULL) {
|
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,
|
(void)snprintf(activity,
|
||||||
sizeof(activity),
|
sizeof(activity),
|
||||||
"uid=%u pid=%d %s(%s) %s",
|
"%s %s(%s) %s",
|
||||||
(unsigned int)caller.uid,
|
caller.caller_id,
|
||||||
(int)caller.pid,
|
|
||||||
method,
|
method,
|
||||||
role_name,
|
role_name,
|
||||||
verdict);
|
verdict);
|
||||||
@@ -935,7 +1102,9 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
|||||||
|
|
||||||
free(request);
|
free(request);
|
||||||
free(response);
|
free(response);
|
||||||
close(client_fd);
|
if (client_fd != STDIN_FILENO) {
|
||||||
|
close(client_fd);
|
||||||
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,7 +1114,9 @@ void server_stop(server_ctx_t *ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ctx->listen_fd >= 0) {
|
if (ctx->listen_fd >= 0) {
|
||||||
close(ctx->listen_fd);
|
if (ctx->listen_mode == NSIGNER_LISTEN_UNIX || ctx->listen_mode == NSIGNER_LISTEN_TCP) {
|
||||||
|
close(ctx->listen_fd);
|
||||||
|
}
|
||||||
ctx->listen_fd = -1;
|
ctx->listen_fd = -1;
|
||||||
}
|
}
|
||||||
ctx->running = 0;
|
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