v0.0.17 - Fix qrexec bridge: server now consumes qrexec_source preamble frame before the JSON-RPC request

This commit is contained in:
Laan Tungir
2026-08-20 18:27:45 -04:00
parent a428d5e77b
commit b5b58ecb87
7 changed files with 1642 additions and 5 deletions
Generated
+1 -1
View File
@@ -2207,7 +2207,7 @@ dependencies = [
[[package]]
name = "signer"
version = "0.0.16"
version = "0.0.17"
dependencies = [
"base64",
"chacha20poly1305",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "signer"
version = "0.0.16"
version = "0.0.17"
edition = "2021"
license = "MIT"
description = "Attended Nostr signing daemon — Rust port of n_signer"
Executable
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# qdiag.sh — read-only Qubes dom0 diagnostics for SignerRpc qrexec issues.
# Run as root in dom0: sudo bash qdiag.sh
# Output: qdiag.txt in the current directory. No system changes are made.
OUT="qdiag.txt"
: >"$OUT"
sec() { printf '\n===== %s =====\n' "$1" >>"$OUT"; }
run() { printf '\n$ %s\n' "$*" >>"$OUT"; "$@" >>"$OUT" 2>&1 || true; }
runsh() { printf '\n$ %s\n' "$1" >>"$OUT"; bash -c "$1" >>"$OUT" 2>&1 || true; }
sec "1. Identity / versions"
run hostname
run id
run uname -a
run cat /etc/qubes-release
run qubesctl --version
run rpm -q qubes-core-admin-linux qubes-core-qrexec 2>/dev/null
sec "2. Qubes version detail"
run qvm-ls --raw-data --fields NAME,STATE,CLASS,TEMPLATE 2>/dev/null
run qvm-ls --running 2>/dev/null
sec "3. Policy directories inventory"
run ls -la /etc/qubes/policy.d/
run ls -la /etc/qubes-rpc/policy/ 2>/dev/null
run ls -la /usr/share/qubes/policy.d/ 2>/dev/null
run ls -la /etc/qubes-rpc/ 2>/dev/null
sec "4. Policy file ownership/permissions"
runsh 'find /etc/qubes/policy.d /etc/qubes-rpc/policy /usr/share/qubes/policy.d -maxdepth 1 -type f -printf "%M %u:%g %s %p\n" 2>/dev/null | sort'
sec "5. Symlinks in policy dirs"
runsh 'find /etc/qubes/policy.d /etc/qubes-rpc /etc/qubes-rpc/policy -maxdepth 2 -type l -printf "%p -> %l\n" 2>/dev/null'
sec "6. Search for SignerRpc / nsigner / signer policy files"
runsh 'find / -xdev \( -path /proc -o -path /sys -o -path /dev \) -prune -o -iname "*signer*" -print 2>/dev/null | grep -vi "^/home" | head -50'
runsh 'grep -RIl "SignerRpc\|NsignerRpc\|nsigner" /etc/qubes /usr/share/qubes 2>/dev/null | head -30'
sec "7. Policy contents (signer-related)"
runsh 'for f in $(grep -RIl "SignerRpc\|NsignerRpc\|nsigner\|signer" /etc/qubes/policy.d /etc/qubes-rpc/policy /usr/share/qubes/policy.d 2>/dev/null); do echo "--- $f ---"; cat "$f"; echo; done'
sec "8. Clipboard policy contents"
runsh 'for f in $(grep -RIl "ClipboardPaste" /etc/qubes/policy.d /etc/qubes-rpc/policy /usr/share/qubes/policy.d 2>/dev/null); do echo "--- $f ---"; cat "$f"; echo; done'
sec "9. Full policy.d listing with contents (all files)"
runsh 'for f in /etc/qubes/policy.d/*; do echo "--- $f ---"; cat "$f" 2>/dev/null; echo; done'
sec "10. Effective policy query (if tools exist)"
runsh 'command -v qrexec-policy-graph && qrexec-policy-graph --include-ask 2>&1 | grep -iE "signer|clipboard" | head -30'
runsh 'command -v qvm-tags && qvm-tags dom0 2>/dev/null | head -20'
sec "11. qrexec policy daemon status"
run systemctl status qrexec-policy-daemon --no-pager -l 2>&1
run systemctl is-active qrexec-policy-daemon 2>&1
sec "12. Target qube info (nostr_signer)"
run qvm-ls --raw-data --fields NAME,STATE,CLASS,TEMPLATE,NETVM nostr_signer 2>/dev/null
run qvm-features nostr_signer 2>/dev/null
run qvm-prefs nostr_signer 2>/dev/null
sec "13. Caller qube info (ai)"
run qvm-ls --raw-data --fields NAME,STATE,CLASS,TEMPLATE,NETVM ai 2>/dev/null
run qvm-features ai 2>/dev/null
sec "14. Recent qrexec/policy journal errors"
runsh 'journalctl -b --no-pager 2>/dev/null | grep -iE "qrexec|policy|SignerRpc|NsignerRpc|clipboard" | tail -80'
sec "15. qrexec service definitions in target qube (via qvm-run, read-only)"
runsh 'qvm-run -p nostr_signer "ls -la /etc/qubes-rpc/ 2>/dev/null; echo ---; ls -la /rw/config/qubes-rpc/ 2>/dev/null; echo ---; cat /rw/config/rc.local 2>/dev/null" 2>&1 | head -60'
sec "16. Test qrexec call to nostr_signer (read-only get_info)"
runsh 'echo "{\"id\":\"diag\",\"method\":\"get_info\",\"params\":[]}" | timeout 10 qrexec-client-vm nostr_signer qubes.SignerRpc 2>&1; echo "exit=$?"'
runsh 'echo "{\"id\":\"diag\",\"method\":\"get_info\",\"params\":[]}" | timeout 10 qrexec-client-vm nostr_signer qubes.NsignerRpc 2>&1; echo "exit=$?"'
sec "17. Qubes global config files"
runsh 'ls -la /etc/qubes/ | head -30'
runsh 'cat /etc/qubes/policy.d/50-config-input.policy 2>/dev/null'
runsh 'cat /etc/qubes/policy.d/50-config-updates.policy 2>/dev/null'
sec "DONE"
printf 'Diagnostics complete. Output saved to %s\n' "$OUT"
exit 0
+1445
View File
File diff suppressed because it is too large Load Diff
Executable
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# qfix.sh — repair dom0 qrexec policy + signer handler. Run as root in dom0:
# sudo bash qfix.sh
# Fixes (with backups saved to ~/qfix-backup-<ts>/):
# 1. Removes invalid old-format file /etc/qubes-rpc/policy/qubes.SignerRpc
# (its "* * * allow" content breaks ALL qrexec policy loading)
# 2. Rewrites /etc/qubes/policy.d/45-signer.policy with direct targets
# 3. Sets the signer-signer tag on nostr_signer (optional, for tag rules)
# 4. Creates /etc/qubes-rpc/qubes.SignerRpc handler inside nostr_signer
# (modeled on the existing qubes.NsignerRpc handler)
# 5. Verifies policy loads cleanly
set -u
TS=$(date +%Y%m%d-%H%M%S)
BK=~/qfix-backup-$TS
mkdir -p "$BK"
ok() { printf '\033[1;32m[OK]\033[0m %s\n' "$*"; }
info(){ printf '\033[1;34m[..]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[ERR]\033[0m %s\n' "$*"; }
[[ $EUID -eq 0 ]] || { err "Run as root: sudo bash qfix.sh"; exit 1; }
# ── 1. Remove the invalid old-format policy file ─────────────────────
if [[ -f /etc/qubes-rpc/policy/qubes.SignerRpc ]]; then
cp /etc/qubes-rpc/policy/qubes.SignerRpc "$BK/" 2>/dev/null
rm -f /etc/qubes-rpc/policy/qubes.SignerRpc
ok "removed invalid /etc/qubes-rpc/policy/qubes.SignerRpc (backed up)"
else
ok "no invalid old-format file present"
fi
# ── 2. Rewrite 45-signer.policy with direct target rules ─────────────
SIGNER_POLICY=/etc/qubes/policy.d/45-signer.policy
if [[ -f "$SIGNER_POLICY" ]]; then
cp "$SIGNER_POLICY" "$BK/45-signer.policy"
fi
cat > "$SIGNER_POLICY" <<'EOF'
# Qubes OS qrexec policy for signer (qubes.SignerRpc)
# Direct rules: caller -> nostr_signer
qubes.SignerRpc * ai nostr_signer allow
qubes.SignerRpc * nostr nostr_signer allow
qubes.SignerRpc * @anyvm @anyvm ask default_target=nostr_signer
EOF
chown root:root "$SIGNER_POLICY"
chmod 0644 "$SIGNER_POLICY"
ok "rewrote $SIGNER_POLICY"
# ── 3. Tag nostr_signer (enables @tag:signer-signer rules if ever used) ──
qvm-tags nostr_signer add signer-signer 2>/dev/null \
&& ok "tagged nostr_signer with signer-signer" \
|| info "tag set skipped (non-fatal)"
# ── 4. Create the qrexec handler inside nostr_signer ─────────────────
info "inspecting existing handlers in nostr_signer..."
qvm-run -p nostr_signer 'cat /etc/qubes-rpc/qubes.NsignerRpc 2>/dev/null' || true
# Detect signer binary location in the target qube
BIN=$(qvm-run -p nostr_signer \
'for b in $HOME/.local/bin/signer /usr/local/bin/signer; do [ -x "$b" ] && echo "$b" && break; done' 2>/dev/null | tr -d '\r')
[[ -n "$BIN" ]] || { err "signer binary not found in nostr_signer (run install_signer.sh there first)"; BIN="/home/user/.local/bin/signer"; }
info "signer binary in nostr_signer: $BIN"
qvm-run -u root -p nostr_signer "printf '#!/bin/sh\nexec $BIN bridge\n' > /etc/qubes-rpc/qubes.SignerRpc && chmod 0755 /etc/qubes-rpc/qubes.SignerRpc" \
&& ok "created /etc/qubes-rpc/qubes.SignerRpc in nostr_signer" \
|| err "handler creation failed (create manually)"
# Show what we created
qvm-run -p nostr_signer 'ls -la /etc/qubes-rpc/qubes.SignerRpc; cat /etc/qubes-rpc/qubes.SignerRpc' || true
# ── 5. Verify policy loads cleanly ───────────────────────────────────
info "verifying policy syntax..."
if qrexec-policy-graph --include-ask >/dev/null 2>"$BK/policy-graph.err"; then
ok "policy loads cleanly (no syntax errors)"
else
err "policy still has errors:"
cat "$BK/policy-graph.err"
fi
echo
ok "repair complete. Backups in $BK"
echo "Now test from the ai qube:"
echo " signer-client --qrexec nostr_signer:qubes.SignerRpc --role main --path \"m/44'/1237'/0'/0/0\" get-public-key"
echo "Clipboard (dom0 -> ai) should also work again: Ctrl+Shift+C in dom0, Ctrl+Shift+V in ai"
+1 -1
View File
@@ -31,4 +31,4 @@ pub mod error;
pub use error::SignerError;
/// Version string (matches C NSIGNER_VERSION).
pub const VERSION: &str = "v0.0.16";
pub const VERSION: &str = "v0.0.17";
+26 -2
View File
@@ -159,7 +159,7 @@ impl ServerContext {
// Read framed request. A connection with no data yet
// (WouldBlock) or an empty/closed probe is not a handled
// request — return Ok(None) so we don't log it as handled.
let request = match crate::transport::recv_framed(&mut reader) {
let mut request = match crate::transport::recv_framed(&mut reader) {
Ok(r) => r,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
return Ok(None);
@@ -168,7 +168,31 @@ impl ServerContext {
};
// Identify caller via SO_PEERCRED
let caller = identify_unix_caller(&reader);
let mut caller = identify_unix_caller(&reader);
// Bridge preamble: `signer bridge` (qrexec relay) sends a
// {"qrexec_source":"<qube>"} frame before the actual
// JSON-RPC request. Consume it, record the source qube,
// and read the real request that follows.
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&request) {
if v.get("qrexec_source").is_some() && v.get("method").is_none() {
caller.source_qube = v
.get("qrexec_source")
.and_then(|s| s.as_str())
.unwrap_or("")
.to_string();
if !caller.source_qube.is_empty() {
caller.caller_id = format!("qubes:{}", caller.source_qube);
}
request = match crate::transport::recv_framed(&mut reader) {
Ok(r) => r,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
return Ok(None);
}
Err(_) => return Ok(None),
};
}
}
// Process request (role-name-as-password model: no authorization)
let (response, activity) = self.process_request(dispatcher, &request, &caller);