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

```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.

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)

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