5 Commits
13 changed files with 2125 additions and 10 deletions
Generated
+1 -1
View File
@@ -2207,7 +2207,7 @@ dependencies = [
[[package]]
name = "signer"
version = "0.0.13"
version = "0.0.18"
dependencies = [
"base64",
"chacha20poly1305",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "signer"
version = "0.0.13"
version = "0.0.18"
edition = "2021"
license = "MIT"
description = "Attended Nostr signing daemon — Rust port of n_signer"
+124
View File
@@ -0,0 +1,124 @@
#!/bin/bash
set -e
# signer (Rust) — Local Deploy Script
#
# Builds release binaries and installs them to /usr/local/bin/.
#
# USAGE:
# ./deploy_local.sh # build release + install
# ./deploy_local.sh --debug # build debug + install
# ./deploy_local.sh -h, --help
#
# Installs:
# /usr/local/bin/signer (the signing daemon)
# /usr/local/bin/signer-client (the CLI client)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${BLUE}[INFO]${NC} $1" >&2; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; }
print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
PROFILE="release"
CARGO_FLAG="--release"
TARGET_DIR="target/release"
show_usage() {
echo "signer (Rust) Local Deploy Script"
echo ""
echo "USAGE:"
echo " $0 [OPTIONS]"
echo ""
echo "OPTIONS:"
echo " --debug Build debug profile instead of release"
echo " -h, --help Show this help message"
echo ""
echo "Installs to /usr/local/bin/:"
echo " signer (the signing daemon)"
echo " signer-client (the CLI client)"
}
while [[ $# -gt 0 ]]; do
case $1 in
--debug)
PROFILE="debug"
CARGO_FLAG=""
TARGET_DIR="target/debug"
shift
;;
-h|--help)
show_usage
exit 0
;;
*)
print_error "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Check we're in the project root (Cargo.toml present)
if [[ ! -f "Cargo.toml" ]]; then
print_error "Cargo.toml not found. Run this script from the project root."
exit 1
fi
# Check the binaries we expect are declared
if ! grep -q 'name = "signer"' Cargo.toml || ! grep -q 'name = "signer-client"' Cargo.toml; then
print_error "Expected binaries 'signer' and 'signer-client' not found in Cargo.toml"
exit 1
fi
# Build
print_status "Building ${PROFILE} binaries (cargo build ${CARGO_FLAG})..."
cargo build ${CARGO_FLAG} 2>&1 | tail -5 || {
print_error "Build failed"
exit 1
}
SIGNER_BIN="${TARGET_DIR}/signer"
CLIENT_BIN="${TARGET_DIR}/signer-client"
for bin in "$SIGNER_BIN" "$CLIENT_BIN"; do
if [[ ! -f "$bin" ]]; then
print_error "Built binary not found: $bin"
exit 1
fi
done
print_success "Binaries built: $SIGNER_BIN, $CLIENT_BIN"
# Install to /usr/local/bin
DEST_DIR="/usr/local/bin"
if [[ ! -d "$DEST_DIR" ]]; then
print_status "Creating $DEST_DIR..."
sudo mkdir -p "$DEST_DIR"
fi
install_binary() {
local src="$1"
local name
name=$(basename "$src")
print_status "Installing $name to $DEST_DIR/..."
sudo install -m 0755 "$src" "$DEST_DIR/$name"
print_success "Installed: $DEST_DIR/$name"
}
install_binary "$SIGNER_BIN"
install_binary "$CLIENT_BIN"
# Verify
print_status "Verification:"
"$DEST_DIR/signer" --version || true
"$DEST_DIR/signer-client" --version || true
print_success "Local deploy completed (${PROFILE} profile)"
+287
View File
@@ -0,0 +1,287 @@
#!/usr/bin/env bash
set -euo pipefail
# User-only installer for Qubes AppVM persistence model.
# Nothing is written to /usr, /etc, or other root-owned paths.
#
# Installs into $HOME:
# - signer -> ~/.local/bin/signer
# - signer-client -> ~/.local/bin/signer-client
# - startup helper -> ~/start_signer.sh
#
# Usage:
# bash install_signer.sh
# bash install_signer.sh --help
#
# Optional env vars:
# SIGNER_VERSION=vX.Y.Z # optional override; default is latest release tag
# SIGNER_GITEA_TOKEN=<token> # if signer release assets are private
# SIGNER_BINARY_URL=<direct url to signer binary>
# SIGNER_CLIENT_BINARY_URL=<direct url to signer-client binary>
SIGNER_VERSION="${SIGNER_VERSION:-}"
PREFIX_BIN="${HOME}/.local/bin"
log() { printf "\033[1;34m[INFO]\033[0m %s\n" "$*"; }
warn() { printf "\033[1;33m[WARN]\033[0m %s\n" "$*"; }
err() { printf "\033[1;31m[ERR ]\033[0m %s\n" "$*"; }
show_help() {
cat <<EOF
Usage: bash install_signer.sh [options]
User-only install (Qubes AppVM friendly):
- signer ${SIGNER_VERSION:-(latest)}
- signer-client ${SIGNER_VERSION:-(latest)}
- signer startup helper script
Options:
-h, --help Show this help and exit
Optional env vars:
SIGNER_VERSION=vX.Y.Z # optional override; default is latest release tag
SIGNER_GITEA_TOKEN=<token> # required if signer release assets are private
SIGNER_BINARY_URL=<direct url to signer binary>
SIGNER_CLIENT_BINARY_URL=<direct url to signer-client binary>
Install paths:
~/.local/bin/signer
~/.local/bin/signer-client
~/start_signer.sh
EOF
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
err "Missing command: $1"
exit 1
}
}
install_runtime_deps() {
if command -v apt-get >/dev/null 2>&1; then
log "Installing runtime dependencies via apt"
sudo apt-get update
sudo apt-get install -y ca-certificates curl jq
elif command -v dnf >/dev/null 2>&1; then
log "Installing runtime dependencies via dnf"
sudo dnf install -y ca-certificates curl jq
else
err "Unsupported distro: need apt-get or dnf to install runtime dependencies"
exit 1
fi
}
prepare_dirs() {
mkdir -p "${PREFIX_BIN}"
}
resolve_signer_version() {
local headers=()
local latest_tag=""
if [[ -n "${SIGNER_VERSION}" ]]; then
return 0
fi
if [[ -n "${SIGNER_GITEA_TOKEN:-}" ]]; then
headers=(-H "Authorization: token ${SIGNER_GITEA_TOKEN}")
fi
latest_tag="$(curl -fsSL "${headers[@]}" "https://git.laantungir.net/api/v1/repos/laantungir/signer/releases" \
| jq -r '.[0].tag_name // empty' || true)"
if [[ -z "${latest_tag}" ]]; then
err "Could not resolve latest signer release tag from API."
err "Set SIGNER_VERSION explicitly (e.g. SIGNER_VERSION=v0.0.14)."
exit 1
fi
SIGNER_VERSION="${latest_tag}"
}
# Resolve a release asset URL by asset name suffix.
# $1 = asset name to match exactly (e.g. "signer" or "signer-client")
download_signer_asset_url() {
local asset_name="$1"
local headers=()
local api_tag_url="https://git.laantungir.net/api/v1/repos/laantungir/signer/releases/tags/${SIGNER_VERSION}"
if [[ -n "${SIGNER_GITEA_TOKEN:-}" ]]; then
headers=(-H "Authorization: token ${SIGNER_GITEA_TOKEN}")
fi
curl -fsSL "${headers[@]}" "${api_tag_url}" \
| jq -r '.assets[]?.browser_download_url // empty' \
| grep -E "/${asset_name}$" \
| head -n1 || true
}
verify_installed_version() {
local expected="$1"
local bin_path="$2"
local got_line=""
local got_ver=""
if [[ ! -x "${bin_path}" ]]; then
err "Installed binary missing: ${bin_path}"
exit 1
fi
got_line="$("${bin_path}" --version 2>/dev/null || true)"
got_ver="$(printf '%s\n' "${got_line}" | awk '{print $2}')"
if [[ -z "${got_ver}" ]]; then
err "Could not determine installed signer version from: ${got_line}"
exit 1
fi
if [[ "${got_ver}" != "${expected}" ]]; then
err "Downloaded binary version mismatch: expected ${expected}, got ${got_ver}"
err "Release asset appears stale or mislabeled."
err "Use SIGNER_BINARY_URL to pin a known-good binary, or wait for a rebuilt release artifact."
exit 1
fi
}
# Download a single binary asset.
# $1 = asset name (for URL resolution fallback)
# $2 = override URL env var name (e.g. SIGNER_BINARY_URL)
# $3 = output path
download_binary() {
local asset_name="$1"
local override_env="$2"
local out_path="$3"
local asset_url=""
# Resolve override from env var
eval "asset_url=\"\${${override_env}:-}\""
if [[ -z "${asset_url}" ]]; then
asset_url="$(download_signer_asset_url "${asset_name}")"
fi
if [[ -z "${asset_url}" ]]; then
err "Could not find downloadable ${asset_name} release binary for ${SIGNER_VERSION}."
err "Provide ${override_env} or SIGNER_GITEA_TOKEN so the release asset can be resolved."
exit 1
fi
log "Using ${asset_name} binary URL: ${asset_url}"
if [[ -n "${SIGNER_GITEA_TOKEN:-}" ]]; then
curl -fL -H "Authorization: token ${SIGNER_GITEA_TOKEN}" -o "${out_path}" "${asset_url}"
else
curl -fL -o "${out_path}" "${asset_url}"
fi
chmod 0755 "${out_path}"
}
install_signer() {
local release_page=""
resolve_signer_version
release_page="https://git.laantungir.net/laantungir/signer/releases/tag/${SIGNER_VERSION}"
log "Installing signer ${SIGNER_VERSION}"
log "Release page: ${release_page}"
# Install the signer daemon binary
download_binary "signer" "SIGNER_BINARY_URL" "${PREFIX_BIN}/signer"
verify_installed_version "${SIGNER_VERSION}" "${PREFIX_BIN}/signer"
log "Installed ${PREFIX_BIN}/signer from release binary"
# Install the signer-client CLI binary (best-effort: older releases may not have it)
if [[ -z "${SIGNER_CLIENT_BINARY_URL:-}" ]] && ! download_signer_asset_url "signer-client" >/dev/null 2>&1; then
warn "No signer-client asset found for ${SIGNER_VERSION}; skipping client install."
else
download_binary "signer-client" "SIGNER_CLIENT_BINARY_URL" "${PREFIX_BIN}/signer-client"
log "Installed ${PREFIX_BIN}/signer-client from release binary"
fi
}
write_signer_start_script() {
local script_path="${HOME}/start_signer.sh"
cat >"${script_path}" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
export PATH="$HOME/.local/bin:$PATH"
LISTEN_TARGET="${SIGNER_LISTEN_TARGET:-tcp:[::]:8080}"
echo "=== signer startup ==="
echo "listen target: ${LISTEN_TARGET}"
# Optional: print current FIPS identity info if fipsctl is available.
if command -v fipsctl >/dev/null 2>&1; then
if fipsctl show status >/dev/null 2>&1; then
STATUS_JSON="$(fipsctl show status)"
elif sudo -n fipsctl show status >/dev/null 2>&1; then
STATUS_JSON="$(sudo -n fipsctl show status)"
else
STATUS_JSON=""
fi
if [[ -n "${STATUS_JSON}" ]]; then
FIPS_IPV6="$(printf '%s\n' "${STATUS_JSON}" | sed -n 's/.*"ipv6_addr": "\([^"]*\)".*/\1/p')"
FIPS_NPUB="$(printf '%s\n' "${STATUS_JSON}" | sed -n 's/.*"npub": "\([^"]*\)".*/\1/p')"
LISTEN_PORT="$(printf '%s\n' "${LISTEN_TARGET}" | sed -n 's/.*:\([0-9][0-9]*\)$/\1/p')"
[[ -n "${FIPS_IPV6}" ]] && echo "fips ipv6: ${FIPS_IPV6}"
[[ -n "${FIPS_NPUB}" ]] && echo "fips npub: ${FIPS_NPUB}"
if [[ -n "${FIPS_NPUB}" && -n "${LISTEN_PORT}" ]]; then
echo "fips address: http://${FIPS_NPUB}.fips:${LISTEN_PORT}"
fi
else
echo "fips status: unavailable (run as user in fips group or with sudo)"
fi
fi
echo
echo "Starting signer..."
echo "On first remote request, approve in prompt with [y] or [a]."
exec "$HOME/.local/bin/signer" --listen "${LISTEN_TARGET}"
EOF
chmod 0755 "${script_path}"
log "Wrote ${script_path}"
}
post_checks() {
export PATH="${PREFIX_BIN}:${PATH}"
log "Running post-install checks"
require_cmd signer
signer --version || true
if [[ -x "${PREFIX_BIN}/signer-client" ]]; then
signer-client --version || true
fi
log "User binaries installed in: ${PREFIX_BIN}"
log "If needed, add to shell PATH: export PATH=\"${PREFIX_BIN}:\$PATH\""
}
main() {
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
show_help
exit 0
fi
if [[ $# -gt 0 ]]; then
err "Unknown option: $1"
show_help
exit 1
fi
install_runtime_deps
prepare_dirs
install_signer
write_signer_start_script
post_checks
log "Completed user-only install of signer"
log "Start signer with: ~/start_signer.sh"
}
main "$@"
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"
+51 -1
View File
@@ -7,7 +7,57 @@ use clap::{Parser, Subcommand};
#[command(
name = "signer-client",
version = ::signer::VERSION,
about = "Standalone CLI for the signer JSON-RPC API"
about = "Standalone CLI for the signer JSON-RPC API",
after_help = "EXAMPLES:
# List running signer sockets
signer-client list
# Get signer metadata (auto-discovers the single running socket)
signer-client get-info
# Get a Nostr public key by role + path
signer-client --role main --path \"m/44'/1237'/0'/0/0\" get-public-key
# Get a public key by algorithm + index
signer-client --algorithm ed25519 --index 0 get-public-key
# Sign a raw message (hex) with ed25519
signer-client --algorithm ed25519 --index 0 sign 68656c6c6f
# Verify a signature (exit 0 = valid, 1 = invalid)
signer-client --algorithm ed25519 --index 0 verify <msg-hex> <sig-hex>
# Sign a Nostr event from stdin and pipe to nak for publishing
echo '{\"kind\":1,\"content\":\"hello nostr\",\"tags\":[],\"created_at\":1700000000}' \\
| signer-client --role main --path \"m/44'/1237'/0'/0/0\" sign-event \\
| nak publish
# NIP-44 encrypt a message to a peer
signer-client --role main --path \"m/44'/1237'/0'/0/0\" nip44-encrypt <peer-pubkey> 'secret'
# NIP-44 decrypt
signer-client --role main --path \"m/44'/1237'/0'/0/0\" nip44-decrypt <peer-pubkey> <ciphertext>
# Derive an HMAC digest (secp256k1)
signer-client --algorithm secp256k1 --index 0 derive testdata
# Raw JSON-RPC passthrough
echo '[]' | signer-client call get_info
# Explicit socket name
signer-client -n signer01 get-info
# TCP transport (requires auth envelope privkey)
signer-client --tcp host:port --auth-privkey <64-hex> get-info
# Serial transport (USB CDC-ACM)
signer-client --serial /dev/ttyACM0 get-info
# Qubes qrexec transport
signer-client --qrexec sys-signer:qubes.SignerRpc get-info
Run 'signer-client <verb> --help' for verb-specific details."
)]
pub struct Cli {
/// Abstract socket name (without @ prefix). Default: auto-discover.
+16 -4
View File
@@ -359,9 +359,16 @@ fn handle_nostr_verb(
// Ensure key is derived. For variable-path roles, use the concrete
// path supplied by the client; for fixed-path roles, use the stored
// template.
if !role.derived {
let has_variable = role.has_variable_path();
// template. Variable-path roles are re-derived whenever the requested
// path differs from the currently-derived one (the cache is per-path,
// not per-role).
let has_variable = role.has_variable_path();
let needs_derive = if has_variable && sel.has_role_path {
role.derived_path.as_deref() != Some(sel.role_path.as_str())
} else {
!role.derived
};
if needs_derive {
let result = if has_variable && sel.has_role_path {
ctx.key_store
.derive_one_with_path(ctx.role_table, ctx.mnemonic, role_index, &sel.role_path)
@@ -414,7 +421,12 @@ fn handle_nostr_verb(
None => return make_error_response(id, RpcError::INVALID_PARAMS),
};
match ctx.key_store.sign_event(role_index, event_json) {
Ok(signed) => make_success_response(id, &format!("\"{}\"", signed)),
// The signed event is itself JSON; serialize it as a proper
// JSON string value so embedded quotes are escaped.
Ok(signed) => {
let wrapped = serde_json::to_string(&signed).unwrap_or_else(|_| "\"\"".into());
make_success_response(id, &wrapped)
}
Err(_) => make_error_response(id, RpcError::INVALID_PARAMS),
}
}
+2
View File
@@ -133,10 +133,12 @@ impl KeyStore {
role.derived = false;
role.pubkey_hex.clear();
role.derived_path = None;
let dk = derive_for_role(concrete_path, role, phrase)?;
role.pubkey_hex = dk.pubkey_hex.clone();
role.derived = true;
role.derived_path = Some(concrete_path.to_string());
if self.keys.len() <= role_index {
self.keys.resize_with(role_index + 1, || None);
+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.13";
pub const VERSION: &str = "v0.0.18";
+3
View File
@@ -137,6 +137,8 @@ pub struct RoleEntry {
pub pubkey_hex: String,
/// 1 if pubkey_hex has been populated.
pub derived: bool,
/// The concrete path the key was last derived for (variable-path roles).
pub derived_path: Option<String>,
/// Inclusive lower bound for %d; -1 = fixed path (no variable).
pub path_range_lo: i32,
/// Inclusive upper bound; == path_range_lo for single.
@@ -162,6 +164,7 @@ impl Default for RoleEntry {
role_path: String::new(),
pubkey_hex: String::new(),
derived: false,
derived_path: None,
path_range_lo: -1,
path_range_hi: -1,
path_default_index: -1,
+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);