Files
n_signer/documents/QUBES_OS.md

7.7 KiB

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.

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:<source-vm>.
  • nsigner --listen qrexec --auth optional: if a caller includes a valid auth envelope, identity becomes qubes:<source-vm>+pubkey:<hex>.
  • nsigner --listen qrexec --auth required: all requests must carry valid envelopes and identity is always qubes:<source-vm>+pubkey:<hex>.

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:

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:

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.

If qrexec auth envelopes are enabled (--auth optional or --auth required), approved callers can also be recorded as composite identities:

  • qubes:<vm>+pubkey:<hex>

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:<source-vm> caller prefix
  • with --auth optional, confirm envelope-enabled clients show qubes:<source-vm>+pubkey:<hex>
  • 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)

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)

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:
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

# 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:<vm> in activity.
  6. README + docs include full setup and troubleshooting.