# 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:` 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:` where available. - qrexec policy in dom0 remains first enforcement boundary. - `n_signer` policy/approval remains second boundary. ### 3.4 Optional per-program identity inside one qube (`--auth optional`) qrexec now supports an optional auth-envelope layer: - `nsigner --listen qrexec --auth off` (default): identity remains `qubes:`. - `nsigner --listen qrexec --auth optional`: if a caller includes a valid auth envelope, identity becomes `qubes:+pubkey:`. - `nsigner --listen qrexec --auth required`: all requests must carry valid envelopes and identity is always `qubes:+pubkey:`. This enables per-program approvals inside a single caller qube while preserving existing behavior for legacy callers that do not emit auth envelopes. --- ## 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:` 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. If qrexec auth envelopes are enabled (`--auth optional` or `--auth required`), approved callers can also be recorded as composite identities: - `qubes:+pubkey:` This gives independent approval rows for distinct programs running in the same source qube. ## 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:` caller prefix - with `--auth optional`, confirm envelope-enabled clients show `qubes:+pubkey:` - 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 ## 5.5 USB keyboard firmware workflows in a PCI-USB-controller qube When a full USB controller is assigned directly to a qube via PCI passthrough, HID input does **not** flow through `sys-usb` input proxy policy. In this setup, keyboard-class USB blocking is usually caused by `usbguard` inside the owning qube. ### Scope check (dom0) ```bash qvm-prefs AI klass ``` - `StandaloneVM`: service state changes in the qube root filesystem persist. - `AppVM`: root filesystem state resets at reboot; use `/rw/config/rc.local` for per-qube persistence. ### Disable usbguard immediately (inside AI qube) ```bash sudo systemctl disable --now usbguard sudo systemctl mask usbguard ``` ### Persist behavior across reboot - **StandaloneVM**: the mask persists as-is. - **AppVM**: persist with `/rw/config/rc.local`: ```bash sudo tee /rw/config/rc.local >/dev/null <<'EOF' #!/bin/sh systemctl stop usbguard 2>/dev/null || true systemctl mask usbguard 2>/dev/null || true EOF sudo chmod +x /rw/config/rc.local ``` ### Restart + verify ```bash # dom0 qvm-shutdown --wait AI && qvm-start AI # inside AI qube systemctl is-enabled usbguard lsusb ls /dev/ttyACM* ``` Expected result: `usbguard` is `masked`, the keyboard-class device enumerates, and serial/WebUSB workflows remain usable. ### Security trade-off Disabling `usbguard` removes an important BadUSB defense for that qube. Keep this configuration limited to a dedicated hardware-development qube and avoid using it for general browsing or untrusted USB devices. --- ## 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:` in activity. 6. README + docs include full setup and troubleshooting.