#!/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: # - nsigner -> ~/.local/bin/nsigner # - startup helper -> ~/start_nsigner.sh # # Usage: # bash install_qube_fips_nsigner.sh # bash install_qube_fips_nsigner.sh --help # # Optional env vars: # NSIGNER_VERSION=vX.Y.Z # optional override; default is latest release tag # NSIGNER_GITEA_TOKEN= # if n_signer release assets are private # NSIGNER_BINARY_URL= NSIGNER_VERSION="${NSIGNER_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 < # required if n_signer release assets are private NSIGNER_BINARY_URL= Install paths: ~/.local/bin/nsigner ~/start_nsigner.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_nsigner_version() { local headers=() local latest_tag="" if [[ -n "${NSIGNER_VERSION}" ]]; then return 0 fi if [[ -n "${NSIGNER_GITEA_TOKEN:-}" ]]; then headers=(-H "Authorization: token ${NSIGNER_GITEA_TOKEN}") fi # Use the git tags API and sort by version number, instead of the releases # API which sorts by created_at timestamp. Gitea has a bug where some # releases get epoch-zero timestamps and don't appear in the releases list. latest_tag="$(curl -fsSL "${headers[@]}" "https://git.laantungir.net/api/v1/repos/laantungir/n_signer/tags?limit=50" \ | jq -r '[.[].name] | map(select(startswith("v"))) | sort_by(ltrimstr("v") | split(".") | map(tonumber)) | last // empty' || true)" if [[ -z "${latest_tag}" ]]; then # Fallback: try the releases API (old behavior) latest_tag="$(curl -fsSL "${headers[@]}" "https://git.laantungir.net/api/v1/repos/laantungir/n_signer/releases" \ | jq -r '.[0].tag_name // empty' || true)" fi if [[ -z "${latest_tag}" ]]; then err "Could not resolve latest n_signer release tag from API." err "Set NSIGNER_VERSION explicitly (e.g. NSIGNER_VERSION=v0.0.46)." exit 1 fi NSIGNER_VERSION="${latest_tag}" } download_nsigner_asset_url() { local headers=() local api_tag_url="https://git.laantungir.net/api/v1/repos/laantungir/n_signer/releases/tags/${NSIGNER_VERSION}" if [[ -n "${NSIGNER_GITEA_TOKEN:-}" ]]; then headers=(-H "Authorization: token ${NSIGNER_GITEA_TOKEN}") fi curl -fsSL "${headers[@]}" "${api_tag_url}" \ | jq -r '.assets[]?.browser_download_url // empty' \ | grep -E 'nsigner_static_x86_64$' \ | head -n1 || true } verify_installed_version() { local expected="$1" local got_line="" local got_ver="" if [[ ! -x "${PREFIX_BIN}/nsigner" ]]; then err "Installed binary missing: ${PREFIX_BIN}/nsigner" exit 1 fi got_line="$("${PREFIX_BIN}/nsigner" --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 n_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 NSIGNER_BINARY_URL to pin a known-good binary, or wait for a rebuilt release artifact." exit 1 fi } install_nsigner() { local release_page="" resolve_nsigner_version release_page="https://git.laantungir.net/laantungir/n_signer/releases/tag/${NSIGNER_VERSION}" log "Installing n_signer ${NSIGNER_VERSION}" log "Release page: ${release_page}" local asset_url="${NSIGNER_BINARY_URL:-}" if [[ -z "${asset_url}" ]]; then asset_url="$(download_nsigner_asset_url)" fi if [[ -z "${asset_url}" ]]; then err "Could not find downloadable n_signer x86_64 release binary for ${NSIGNER_VERSION}." err "Provide NSIGNER_BINARY_URL or NSIGNER_GITEA_TOKEN so the release asset can be resolved." exit 1 fi log "Using n_signer binary URL: ${asset_url}" if [[ -n "${NSIGNER_GITEA_TOKEN:-}" ]]; then curl -fL -H "Authorization: token ${NSIGNER_GITEA_TOKEN}" -o "${PREFIX_BIN}/nsigner" "${asset_url}" else curl -fL -o "${PREFIX_BIN}/nsigner" "${asset_url}" fi chmod 0755 "${PREFIX_BIN}/nsigner" verify_installed_version "${NSIGNER_VERSION}" log "Installed ${PREFIX_BIN}/nsigner from release binary" } write_signer_start_script() { local script_path="${HOME}/start_nsigner.sh" cat >"${script_path}" <<'EOF' #!/usr/bin/env bash set -euo pipefail export PATH="$HOME/.local/bin:$PATH" LISTEN_TARGET="${NSIGNER_LISTEN_TARGET:-tcp:[::]:11111}" # If first arg is "qrexec", start in unix bridge mode for Qubes qrexec. # Otherwise, pass any extra args through to nsigner (e.g. --allow-all). if [[ "${1:-}" == "qrexec" ]]; then LISTEN_TARGET="unix" EXTRA_ARGS="--socket-name nsigner --bridge-source-trusted" shift else EXTRA_ARGS="$*" fi echo "=== n_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]." if [[ -n "${EXTRA_ARGS}" ]]; then exec "$HOME/.local/bin/nsigner" --listen "${LISTEN_TARGET}" ${EXTRA_ARGS} else exec "$HOME/.local/bin/nsigner" --listen "${LISTEN_TARGET}" fi EOF chmod 0755 "${script_path}" log "Wrote ${script_path}" } post_checks() { export PATH="${PREFIX_BIN}:${PATH}" log "Running post-install checks" require_cmd nsigner nsigner --version || true 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_nsigner write_signer_start_script post_checks log "Completed user-only install of n_signer" log "Start signer with: ~/start_nsigner.sh" } main "$@"