A GTK4 SSH-askpass in Zig

原始链接: https://xn--gckvb8fzb.com/a-gtk4-ssh-askpass-in-zig/

相关文章

原文

Every ssh-askpass in the Gentoo tree pulls in X11 or a full KDE stack, so I wrote my own with Zig 0.16 and GTK4, with hand-written bindings that keep X out of the build.

I run hardened Gentoo on my laptop, and most of the time I never touch ssh-askpass because I’m using -sk keys for most of the systems. There is one class of situation where I do need it, though, which is when a program wants an SSH key passphrase for a regular ED25519 key, but has no terminal to read it from. The usual case is go get, or the go toolchain in general, fetching a private module over SSH during a build that runs without a TTY. OpenSSH can’t prompt on a pipe, so it runs whatever SSH_ASKPASS points at and puts the passphrase prompt in a window instead. For years I had nothing installed for that and had to work around these scenarios. The main reason for that is what Gentoo’s Portage offers:

 ~ emerge -s ssh-askpass

[ Results for search key : ssh-askpass ]
Searching...

*  kde-plasma/ksshaskpass
      Latest version available: 6.6.6
      Latest version installed: [ Not Installed ]
      Size of files: 32 KiB
      Homepage:      https://kde.org/plasma-desktop https://invent.kde.org/plasma/ksshaskpass
      Description:   Implementation of ssh-askpass with KDE Wallet integration
      License:       GPL-2

*  lxqt-base/lxqt-openssh-askpass
      Latest version available: 2.4.0
      Latest version installed: [ Not Installed ]
      Size of files: 19 KiB
      Homepage:      https://lxqt-project.org/ https://github.com/lxqt/lxqt-openssh-askpass/
      Description:   LXQt OpenSSH user password prompt tool
      License:       LGPL-2.1 LGPL-2.1+

*  net-misc/gnome-ssh-askpass
      Latest version available: 10.0_p1
      Latest version installed: [ Not Installed ]
      Size of files: 1,927 KiB
      Homepage:      https://www.openssh.com/
      Description:   GTK-based passphrase dialog for use with OpenSSH
      License:       BSD GPL-2

*  net-misc/ssh-askpass-fullscreen
      Latest version available: 1.2
      Latest version installed: [ Not Installed ]
      Size of files: 81 KiB
      Homepage:      https://github.com/atj/ssh-askpass-fullscreen
      Description:   A small SSH Askpass replacement written with GTK2
      License:       GPL-2+

*  net-misc/x11-ssh-askpass
      Latest version available: 1.2.4.1-r2
      Latest version installed: [ Not Installed ]
      Size of files: 30 KiB
      Homepage:      https://github.com/sigmavirus24/x11-ssh-askpass
      Description:   X11-based passphrase dialog for use with OpenSSH
      License:       HPND

[ Applications found : 5 ]

Each of these has at least one inconvenience I didn’t feel like putting up with. My system runs with the global -X USE flag, so anything that needs X11 is out before I look any further. Of the five, kde-plasma/ksshaskpass is the only one with no X11 dependency whatsoever, which should have made it the obvious pick, but the trouble is everything else that comes with it. As a Sway user, I did not want a full KDE stack on the machine just to type the occasional passphrase, and that is what a ksshaskpass install pulls in:

~ # emerge -a kde-plasma/ksshaskpass

These are the packages that would be merged, in order:

Calculating dependencies... done!
Dependency resolution took 1.18 s (backtrack: 0/20).

[ebuild  N     ] kde-frameworks/kf-env-6
[ebuild  N     ] dev-libs/plasma-wayland-protocols-1.21.0  USE="-test"
[ebuild  N     ] dev-libs/qtkeychain-0.16.0  USE="-keyring -test"
[ebuild  N     ] kde-frameworks/ksecretd-services-6.27.0  USE="(-systemd)"
[ebuild  N     ] kde-frameworks/breeze-icons-6.27.0  USE="-test"
[ebuild  N     ] kde-frameworks/ki18n-6.27.0  USE="-debug -test" PYTHON_SINGLE_TARGET="python3_14 -python3_12 -python3_13"
[ebuild  N     ] kde-frameworks/kconfig-6.27.0  USE="dbus -debug -qml -test"
[ebuild  N     ] kde-frameworks/kcoreaddons-6.27.0  USE="dbus -debug -test"
[ebuild  N     ] kde-frameworks/kwidgetsaddons-6.27.0  USE="-debug -designer -test"
[ebuild  N     ] kde-frameworks/kdbusaddons-6.27.0  USE="-X -debug -test"
[ebuild  N     ] kde-frameworks/kwindowsystem-6.27.0  USE="wayland -X -debug -test"
[ebuild  N     ] app-crypt/qca-2.3.10-r1  USE="ssl -botan -debug -doc -examples -gcrypt -gpg -logger -nss -pkcs11 -sasl -softstore -test"
[ebuild  N     ] kde-frameworks/karchive-6.27.0  USE="crypt zstd -debug -test"
[ebuild  N     ] kde-frameworks/kguiaddons-6.27.0  USE="dbus wayland -X -debug -test"
[ebuild  N     ] kde-frameworks/kdoctools-6.27.0  USE="nls -debug -test"
[ebuild  N     ] kde-frameworks/kcolorscheme-6.27.0  USE="-debug"
[ebuild  N     ] kde-frameworks/kcrash-6.27.0  USE="-X -debug -test"
[ebuild  N     ] kde-frameworks/knotifications-6.27.0  USE="-debug"
[ebuild  N     ] kde-frameworks/kservice-6.27.0  USE="man -debug -test"
[ebuild  N     ] kde-frameworks/kwallet-6.27.0  USE="-debug -minimal -test"
[ebuild  N     ] kde-frameworks/kwallet-runtime-6.27.0  USE="(keyring) (legacy-kwallet) man -X -debug -gpg -test"
[ebuild  N     ] kde-plasma/ksshaskpass-6.6.6  USE="-debug"

Would you like to merge these packages? [Yes/No]

lxqt-base/lxqt-openssh-askpass is next, and it needs X outright. On top of that it pulls in a few KDE framework packages and a Qt built with X support, which collides with the dev-qt/qtbase already on my system that was compiled -X, so Portage stops on a slot conflict:

~ # emerge -a lxqt-base/lxqt-openssh-askpass

These are the packages that would be merged, in order:

Calculating dependencies... done!
Dependency resolution took 3.10 s (backtrack: 0/20).

[ebuild   R    ] x11-libs/libxkbcommon-1.13.2  USE="X*"
[ebuild  N     ] dev-util/lxqt-build-tools-2.4.0
[ebuild  N     ] x11-libs/xcb-util-renderutil-0.3.10  ABI_X86="(64) -32 (-x32)"
[ebuild  N     ] x11-libs/libXScrnSaver-1.2.5  USE="-doc" ABI_X86="(64) -32 (-x32)"
[ebuild  N     ] dev-libs/plasma-wayland-protocols-1.21.0  USE="-test"
[ebuild  N     ] x11-libs/xcb-util-wm-0.4.2  ABI_X86="(64) -32 (-x32)"
[ebuild  N     ] x11-libs/xcb-util-0.4.1  ABI_X86="(64) -32 (-x32)"
[ebuild  N     ] x11-libs/xcb-util-image-0.4.1  ABI_X86="(64) -32 (-x32)"
[ebuild  N     ] x11-libs/xcb-util-cursor-0.1.6  ABI_X86="(64) -32 (-x32)"
[ebuild   R    ] dev-qt/qtbase-6.11.1  USE="X*"
[ebuild  N     ] sys-auth/polkit-qt-0.201.1
[ebuild  N     ] dev-libs/libqtxdg-4.4.0  USE="-test"
[ebuild  N     ] kde-frameworks/kf-env-6
[ebuild  N     ] kde-frameworks/kwindowsystem-6.27.0  USE="X wayland -debug -test"
[ebuild  N     ] lxqt-base/liblxqt-2.4.0  USE="backlight"
[ebuild  N     ] lxqt-base/lxqt-openssh-askpass-2.4.0

!!! Multiple package instances within a single package slot have been pulled
!!! into the dependency graph, resulting in a slot conflict:

dev-qt/qtbase:6

  (dev-qt/qtbase-6.11.1:6/6.11.1::gentoo, installed) USE="concurrent dbus gui icu libinput libproxy network nls opengl sql sqlite ssl syslog udev vulkan wayland widgets xml (zstd) -X -accessibility -brotli -cups -custom-cflags -eglfs -evdev -gles2-only -gssapi -gtk -io-uring (-journald) -mysql -oci8 -odbc -postgres -renderdoc -sctp -test -tslib" ABI_X86="(64)" pulled in by
    ~dev-qt/qtbase-6.11.1:6[X=,concurrent,eglfs=] required by (dev-qt/qtmultimedia-6.11.1:6/6.11.1::gentoo, installed) USE="alsa dbus ffmpeg gstreamer opengl pipewire qml v4l vaapi vulkan wayland -X -custom-cflags -eglfs -pulseaudio -test" ABI_X86="(64)"
                            ^^

  (dev-qt/qtbase-6.11.1:6/6.11.1::gentoo, ebuild scheduled for merge) USE="X concurrent dbus gui icu libinput libproxy network nls opengl sql sqlite ssl syslog udev vulkan wayland widgets xml (zstd) -accessibility -brotli -cups -custom-cflags -eglfs -evdev -gles2-only -gssapi -gtk -io-uring (-journald) -mysql -oci8 -odbc -postgres -renderdoc -sctp -test -tslib" ABI_X86="(64)" pulled in by
    >=dev-qt/qtbase-6.10.1:6=[gui,X] required by (kde-frameworks/kwindowsystem-6.27.0:6/6.27::gentoo, ebuild scheduled for merge) USE="X wayland -debug -test" ABI_X86="(64)"
                                  ^


It may be possible to solve this problem by using package.mask to
prevent one of those packages from being selected. However, it is also
possible that conflicting dependencies exist such that they are
impossible to satisfy simultaneously.  If such a conflict exists in
the dependencies of two different packages, then those packages can
not be installed simultaneously.

For more information, see MASKED PACKAGES section in the emerge man
page or refer to the Gentoo Handbook.


The following USE changes are necessary to proceed:
 (see "package.use" in the portage(5) man page for more details)
# required by lxqt-base/liblxqt-2.4.0::gentoo
# required by lxqt-base/lxqt-openssh-askpass-2.4.0::gentoo
# required by lxqt-base/lxqt-openssh-askpass (argument)
>=kde-frameworks/kwindowsystem-6.27.0 X
# required by kde-frameworks/kwindowsystem-6.27.0::gentoo[X]
# required by lxqt-base/liblxqt-2.4.0::gentoo
# required by lxqt-base/lxqt-openssh-askpass-2.4.0::gentoo
# required by lxqt-base/lxqt-openssh-askpass (argument)
>=dev-qt/qtbase-6.11.1 X
# required by dev-qt/qtbase-6.11.1::gentoo[gui]
# required by kde-frameworks/kwindowsystem-6.27.0::gentoo[X]
# required by lxqt-base/liblxqt-2.4.0::gentoo
# required by lxqt-base/lxqt-openssh-askpass-2.4.0::gentoo
# required by lxqt-base/lxqt-openssh-askpass (argument)
>=x11-libs/libxkbcommon-1.13.2 X

Would you like to add these changes to your config files? [Yes/No]

net-misc/ssh-askpass-fullscreen needs X as well, this time by way of GTK2 and a Cairo built with X support:

~ # emerge -a net-misc/ssh-askpass-fullscreen

These are the packages that would be merged, in order:

Calculating dependencies... done!
Dependency resolution took 2.98 s (backtrack: 0/20).

[ebuild   R    ] x11-libs/cairo-1.18.4-r1  USE="X*"
[ebuild  N     ] x11-libs/libXcomposite-0.4.7  USE="-doc" ABI_X86="(64) -32 (-x32)"
[ebuild  N     ] x11-libs/libXcursor-1.2.3  USE="-doc" ABI_X86="(64) -32 (-x32)"
[ebuild  N     ] x11-libs/libXdamage-1.1.7  ABI_X86="(64) -32 (-x32)"
[ebuild  N     ] x11-libs/libXi-1.8.3  USE="-doc" ABI_X86="(64) -32 (-x32)"
[ebuild  N     ] x11-themes/gnome-themes-standard-3.28-r1
[ebuild  NS    ] x11-libs/gtk+-2.24.33-r3 [3.24.52]
[ebuild  N     ] x11-themes/gtk-engines-adwaita-3.28-r1  ABI_X86="(64) -32 (-x32)"
[ebuild  N     ] net-misc/ssh-askpass-fullscreen-1.2

The following USE changes are necessary to proceed:
 (see "package.use" in the portage(5) man page for more details)
# required by x11-libs/gtk+-2.24.33-r3::gentoo
# required by x11-themes/gtk-engines-adwaita-3.28-r1::gentoo
>=x11-libs/cairo-1.18.4-r1 X

Would you like to add these changes to your config files? [Yes/No]

net-misc/x11-ssh-askpass is X11 by name, so no surprise there, and it also needs the old imake build system, namely x11-misc/xorg-cf-files and x11-misc/imake, to compile at all:

~ # emerge -a net-misc/x11-ssh-askpass

These are the packages that would be merged, in order:

Calculating dependencies... done!
Dependency resolution took 1.00 s (backtrack: 0/20).

[ebuild  N     ] app-text/rman-3.2-r2
[ebuild  N     ] x11-misc/xorg-cf-files-1.0.9
[ebuild  N     ] x11-misc/imake-1.0.10
[ebuild  N     ] net-misc/x11-ssh-askpass-1.2.4.1-r2

Would you like to merge these packages? [Yes/No]

That left net-misc/gnome-ssh-askpass. At first glance it looked like the one option that needed no X at all, but that turned out to be wrong. It does need X11, and the ebuild appears to be broken about it. The build calls pkg-config --libs gtk+-3.0 x11 and the source includes gdk/gdkx.h, an X-only GDK header, so on a system compiled without X it fails to build:

~ # emerge -a net-misc/gnome-ssh-askpass

These are the packages that would be merged, in order:

Calculating dependencies... done!
Dependency resolution took 0.89 s (backtrack: 0/20).

[ebuild  N    ~] net-misc/gnome-ssh-askpass-10.0_p1  USE="-verify-sig"

Would you like to merge these packages? [Yes/No] Yes
>>> Verifying ebuild manifests
>>> Emerging (1 of 1) net-misc/gnome-ssh-askpass-10.0_p1::guru
>>> Failed to emerge net-misc/gnome-ssh-askpass-10.0_p1, Log file:
>>>  '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/build.log'
>>> Jobs: 0 of 1 complete, 1 failed                                     Load avg: 0.79, 0.57, 0.61
 * Package:    net-misc/gnome-ssh-askpass-10.0_p1:0
 * Repository: guru
 * USE:        abi_x86_64 amd64 elibc_glibc kernel_linux
 * FEATURES:   network-sandbox preserve-libs sandbox selinux sesandbox userpriv usersandbox
>>> Unpacking source...
>>> Unpacking 'openssh-10.0p1.tar.gz' to /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work
>>> Source unpacked in /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work
>>> Preparing source in /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib ...
>>> Source prepared.
>>> Configuring source in /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib ...
>>> Source configured.
>>> Compiling source in /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib ...
make -j17 -l15.95 gnome-ssh-askpass3
x86_64-pc-linux-gnu-gcc -O2 -pipe -march=native `pkg-config --cflags gtk+-3.0` \
        gnome-ssh-askpass3.c -o gnome-ssh-askpass3 \
        `pkg-config --libs gtk+-3.0 x11`
gnome-ssh-askpass3.c:62:10: fatal error: gdk/gdkx.h: No such file or directory
   62 | #include <gdk/gdkx.h>
      |          ^~~~~~~~~~~~
compilation terminated.
make: *** [Makefile:17: gnome-ssh-askpass3] Error 1
 * ERROR: net-misc/gnome-ssh-askpass-10.0_p1::guru failed (compile phase):
 *   emake failed
 *
 * If you need support, post the output of `emerge --info '=net-misc/gnome-ssh-askpass-10.0_p1::guru'`,
 * the complete build log and the output of `emerge -pqv '=net-misc/gnome-ssh-askpass-10.0_p1::guru'`.
 * The complete build log is located at '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/build.log'.
 * The ebuild environment file is located at '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/environment'.
 * Working directory: '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib'
 * S: '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib'

 * Messages for package net-misc/gnome-ssh-askpass-10.0_p1:

 * ERROR: net-misc/gnome-ssh-askpass-10.0_p1::guru failed (compile phase):
 *   emake failed
 * If you need support, post the output of `emerge --info '=net-misc/gnome-ssh-askpass-10.0_p1::guru'`,
 * the complete build log and the output of `emerge -pqv '=net-misc/gnome-ssh-askpass-10.0_p1::guru'`.
 * The complete build log is located at '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/build.log'.
 * The ebuild environment file is located at '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/environment'.
 * Working directory: '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib'
 * S: '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib'
 *
 * The following package has failed to build, install, or execute postinst:
 *
 *  (net-misc/gnome-ssh-askpass-10.0_p1:0/0::guru, ebuild scheduled for merge), Log file:
 *   '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/build.log'
 *

This is where I gave up on the packaged options. Even setting the X11 question aside, every one of these uses GTK2 or GTK3 at most. However, it just so happened that I had wanted to build something with GTK4 for a long time, so instead of patching one of the existing implementations, which are mostly C anyway, I wrote my own with Zig 0.16 and GTK4, and called it ssh-askpass-zigtk.

Avoiding X11

The reason the GTK helpers break on my system is the headers. The standard way of calling GTK includes the GTK4 headers, which pull in GDK, and GDK still ships gdk/gdkx.h on most installs, so an X11 header comes in whether you want it or not. Zig’s @cImport, the obvious way to call a C library, would do the same, because it pulls in exactly those headers. So ssh-askpass-zigtk doesn’t @cImport anything. src/gtk.zig declares the thirty-odd GTK and GLib functions the program calls by hand, as plain extern prototypes:

pub const Widget = opaque {};

pub extern fn gtk_window_new() *Widget;
pub extern fn gtk_password_entry_new() *Widget;
pub extern fn gtk_editable_get_text(editable: *Widget) [*:0]const u8;
pub extern fn gtk_button_new_with_label(label: [*:0]const u8) *Widget;

Nothing in that file names a symbol from gdk/gdkx.h or X11/Xlib.h, so the compiler never sees an X header, and the binary builds and runs against a GTK4 that was compiled without X11. The one X-adjacent value it needs, the Escape keysym, is hardcoded as 0xff1b rather than pulled from gdk/gdkkeysyms.h.

GTK is built on GObject, which does single inheritance by putting the parent struct as the first member of the child, so a window, a box, a label, a password entry and a button are all layout-compatible with a GtkWidget * at the ABI boundary. On the Zig side one opaque {} type stands in for all of them, and every widget function takes and returns the same *Widget, without a hierarchy of wrapper types to model something the C ABI already flattens.

The parts that don’t touch GTK, the mapping of SSH_ASKPASS_PROMPT to a dialog type and the parsing of the GNOME_SSH_ASKPASS_*_COLOR variables, are in src/root.zig with unit tests, so they run under zig build test with no display and no GTK at all. Recoloring goes through a small CSS provider, since GTK4 removed gtk_widget_modify_fg and _bg.

Cross-compiling

Because the bindings are hand-written externs and no GTK headers enter the build, Zig can cross-compile the binary for any Linux architecture without a GTK4 toolchain for that target. The only thing missing at link time is the GTK4 shared library itself, and -Dgtk-stub covers that, as it builds a tiny stub libgtk-4.so.1 whose exported symbols are all no-ops, links the executable against that, and lets the target’s real GTK4 resolve at runtime instead. The release workflow uses this to produce binaries for x86_64, aarch64, armv7, riscv64, powerpc64le, i386, loongarch64 and s390x from one machine, none of which has GTK4 installed for the other seven.

Note: ssh-askpass-zigtk doesn’t grab the keyboard as other askpass implementations normally would. The GTK3 helper calls gdk_seat_grab so another client can’t read the passphrase as you type it, but from what I see, GTK4 dropped that interface and I believe that Wayland doesn’t let a client grab the keyboard at all, so there is no portable way to do it without X11. Hence the GNOME_SSH_ASKPASS_GRAB_SERVER and GNOME_SSH_ASKPASS_GRAB_POINTER variables also have no effect.

The code is on tty.fail and mirrored to GitHub, where each tagged release ships prebuilt Linux binaries per architecture. To use it, put the binary somewhere on your PATH and point SSH_ASKPASS at it. For a terminal that means two lines in ~/.profile or your shell’s startup file (e.g. ~/.zshrc for my fellow Zsh users):

export SSH_ASKPASS=~/.local/bin/ssh-askpass-zigtk
export SSH_ASKPASS_REQUIRE=prefer

SSH_ASKPASS_REQUIRE=prefer, from OpenSSH 8.4 onward, tells OpenSSH to use the dialog even when a terminal is available, as long as a graphical session is present. On a systemd user session, the same two variables go in ~/.config/environment.d/ssh-askpass.conf as plain KEY=VALUE lines with an absolute path, since that file neither expands ~ nor runs a shell. Log out and back in, and the next ssh-add, git pull or go get that needs a passphrase without a terminal gets the dialog.

联系我们 contact @ memedata.com