A GTK4 SSH-askpass in Zig

Jul 31, 2026 10:40 PM - 1 month ago 658

Every ssh-askpass successful the Gentoo character pulls successful X11 aliases a afloat KDE stack, so I wrote my ain pinch Zig 0.16 and GTK4, pinch hand-written bindings that keep X retired of the build.

I tally hardened Gentoo connected my laptop, and astir of the clip I never touch ssh-askpass because I’m utilizing -sk keys for astir of the systems. There is 1 people of business wherever I do request it, though, which is erstwhile a program wants an SSH cardinal passphrase for a regular ED25519 key, but has no terminal to publication it from. The accustomed lawsuit is spell get, aliases the spell toolchain in general, fetching a backstage module complete SSH during a build that runs without a TTY. OpenSSH can’t punctual connected a pipe, truthful it runs immoderate SSH_ASKPASS points at and puts the passphrase punctual successful a model instead. For years I had nothing installed for that and had to activity astir these scenarios. The main logic for that is what Gentoo’s Portage offers:

 ~ look -s ssh-askpass [ Results for hunt cardinal : ssh-askpass ] Searching... * kde-plasma/ksshaskpass Latest type available: 6.6.6 Latest type 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 pinch KDE Wallet integration License: GPL-2 * lxqt-base/lxqt-openssh-askpass Latest type available: 2.4.0 Latest type installed: [ Not Installed ] Size of files: 19 KiB Homepage: https://lxqt-project.org/ https://github.com/lxqt/lxqt-openssh-askpass/ Description: LXQt OpenSSH personification password punctual tool License: LGPL-2.1 LGPL-2.1+ * net-misc/gnome-ssh-askpass Latest type available: 10.0_p1 Latest type installed: [ Not Installed ] Size of files: 1,927 KiB Homepage: https://www.openssh.com/ Description: GTK-based passphrase dialog for usage pinch OpenSSH License: BSD GPL-2 * net-misc/ssh-askpass-fullscreen Latest type available: 1.2 Latest type installed: [ Not Installed ] Size of files: 81 KiB Homepage: https://github.com/atj/ssh-askpass-fullscreen Description: A mini SSH Askpass replacement written pinch GTK2 License: GPL-2+ * net-misc/x11-ssh-askpass Latest type available: 1.2.4.1-r2 Latest type installed: [ Not Installed ] Size of files: 30 KiB Homepage: https://github.com/sigmavirus24/x11-ssh-askpass Description: X11-based passphrase dialog for usage pinch OpenSSH License: HPND [ Applications recovered : 5 ]

Each of these has astatine slightest 1 inconvenience I didn’t consciousness for illustration putting up with. My strategy runs pinch the world -X USE flag, truthful thing that needs X11 is out earlier I look immoderate further. Of the five, kde-plasma/ksshaskpass is the only one pinch no X11 dependency whatsoever, which should person made it the obvious pick, but the problem is everything other that comes pinch it. As a Sway user, I did not want a afloat KDE stack connected the instrumentality conscionable to type the occasional passphrase, and that is what a ksshaskpass instal pulls in:

~ # look -a kde-plasma/ksshaskpass These are the packages that would beryllium merged, successful order: Calculating dependencies... done! Dependency solution 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 for illustration to merge these packages? [Yes/No]

lxqt-base/lxqt-openssh-askpass is next, and it needs X outright. On apical of that it pulls successful a fewer KDE model packages and a Qt built pinch X support, which collides pinch the dev-qt/qtbase already connected my strategy that was compiled -X, truthful Portage stops connected a slot conflict:

~ # look -a lxqt-base/lxqt-openssh-askpass These are the packages that would beryllium merged, successful order: Calculating dependencies... done! Dependency solution 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 wrong a azygous package slot person been pulled !!! into the dependency graph, resulting successful 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 web 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 successful 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 web 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 successful 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 whitethorn beryllium imaginable to lick this problem by utilizing package.mask to prevent 1 of those packages from being selected. However, it is also possible that conflicting limitations beryllium specified that they are impossible to fulfill simultaneously. If specified a conflict exists in the limitations of 2 different packages, past those packages can not beryllium installed simultaneously. For much information, spot MASKED PACKAGES conception successful the look man page aliases mention to the Gentoo Handbook. The pursuing USE changes are basal to proceed: (see "package.use" successful the portage(5) man page for much 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 for illustration to adhd these changes to your config files? [Yes/No]

net-misc/ssh-askpass-fullscreen needs X arsenic well, this clip by measurement of GTK2 and a Cairo built pinch X support:

~ # look -a net-misc/ssh-askpass-fullscreen These are the packages that would beryllium merged, successful order: Calculating dependencies... done! Dependency solution 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 pursuing USE changes are basal to proceed: (see "package.use" successful the portage(5) man page for much 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 for illustration to adhd these changes to your config files? [Yes/No]

net-misc/x11-ssh-askpass is X11 by name, truthful nary astonishment there, and it also needs the aged imake build system, namely x11-misc/xorg-cf-files and x11-misc/imake, to compile astatine all:

~ # look -a net-misc/x11-ssh-askpass These are the packages that would beryllium merged, successful order: Calculating dependencies... done! Dependency solution 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 for illustration to merge these packages? [Yes/No]

That near net-misc/gnome-ssh-askpass. At first glimpse it looked for illustration the one option that needed nary X astatine all, but that turned retired to beryllium wrong. It does need X11, and the ebuild appears to beryllium surgery astir it. The build calls pkg-config --libs gtk+-3.0 x11 and the root includes gdk/gdkx.h, an X-only GDK header, truthful connected a strategy compiled without X it fails to build:

~ # look -a net-misc/gnome-ssh-askpass These are the packages that would beryllium merged, successful order: Calculating dependencies... done! Dependency solution took 0.89 s (backtrack: 0/20). [ebuild N ~] net-misc/gnome-ssh-askpass-10.0_p1 USE="-verify-sig" Would you for illustration 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 look 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 grounded 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 successful /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work >>> Preparing source successful /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib ... >>> Source prepared. >>> Configuring source successful /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib ... >>> Source configured. >>> Compiling source successful /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 specified record aliases 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 grounded (compile phase): * emake failed * * If you request support, station 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 astatine '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/build.log'. * The ebuild situation record is located astatine '/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 grounded (compile phase): * emake failed * If you request support, station 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 astatine '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/build.log'. * The ebuild situation record is located astatine '/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 pursuing package has grounded to build, install, aliases 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 wherever I gave up connected the packaged options. Even mounting the X11 question aside, each 1 of these uses GTK2 aliases GTK3 astatine most. However, it conscionable so happened that I had wanted to build thing pinch GTK4 for a agelong time, so instead of patching 1 of the existing implementations, which are mostly C anyway, I wrote my ain pinch Zig 0.16 and GTK4, and called it ssh-askpass-zigtk.

Avoiding X11

The logic the GTK helpers break connected my strategy is the headers. The modular way of calling GTK includes the GTK4 headers, which propulsion successful GDK, and GDK still ships gdk/gdkx.h connected astir installs, truthful an X11 header comes successful whether you want it aliases not. Zig’s @cImport, the evident measurement to telephone a C library, would do the same, because it pulls successful precisely those headers. So ssh-askpass-zigtk doesn’t @cImport anything. src/gtk.zig declares the thirty-odd GTK and GLib functions the programme calls by hand, arsenic 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 successful that record names a awesome from gdk/gdkx.h aliases X11/Xlib.h, truthful the compiler ne'er sees an X header, and the binary builds and runs against a GTK4 that was compiled without X11. The 1 X-adjacent worth it needs, the Escape keysym, is hardcoded arsenic 0xff1b alternatively than pulled from gdk/gdkkeysyms.h.

GTK is built connected GObject, which does azygous inheritance by putting the parent struct arsenic the first personnel of the child, truthful a window, a box, a label, a password entry and a fastener are each layout-compatible pinch a GtkWidget * astatine the ABI boundary. On the Zig broadside 1 opaque {} type stands successful for each of them, and every widget usability takes and returns the aforesaid *Widget, without a hierarchy of wrapper types to exemplary thing 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 pinch portion tests, truthful they tally nether zig build trial pinch no display and nary GTK astatine all. Recoloring goes done a mini CSS provider, since GTK4 removed gtk_widget_modify_fg and _bg.

Cross-compiling

Because the bindings are hand-written externs and nary GTK headers participate the build, Zig tin cross-compile the binary for immoderate Linux architecture without a GTK4 toolchain for that target. The only point missing astatine nexus clip is the GTK4 shared room itself, and -Dgtk-stub covers that, arsenic it builds a tiny stub libgtk-4.so.1 whose exported symbols are each no-ops, links the executable against that, and lets the target’s existent GTK4 resoluteness astatine runtime instead. The release workflow uses this to nutrient binaries for x86_64, aarch64, armv7, riscv64, powerpc64le, i386, loongarch64 and s390x from 1 machine, none of which has GTK4 installed for the different seven.

Note: ssh-askpass-zigtk doesn’t drawback the keyboard arsenic different askpass implementations usually would. The GTK3 helper calls gdk_seat_grab so another customer can’t publication the passphrase arsenic you type it, but from what I see, GTK4 dropped that interface and I judge that Wayland doesn’t fto a client grab the keyboard astatine all, truthful location is nary portable measurement to do it without X11. Hence the GNOME_SSH_ASKPASS_GRAB_SERVER and GNOME_SSH_ASKPASS_GRAB_POINTER variables besides person nary effect.

The codification is connected tty.fail and mirrored to GitHub, wherever each tagged merchandise ships prebuilt Linux binaries per architecture. To usage it, put the binary location connected your PATH and constituent SSH_ASKPASS astatine it. For a terminal that intends 2 lines in ~/.profile aliases your shell’s startup record (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 moreover erstwhile a terminal is available, arsenic agelong arsenic a graphical convention is present. On a systemd personification session, the aforesaid 2 variables spell in ~/.config/environment.d/ssh-askpass.conf arsenic plain KEY=VALUE lines pinch an absolute path, since that record neither expands ~ nor runs a shell. Log retired and back in, and the adjacent ssh-add, git propulsion aliases spell get that needs a passphrase without a terminal gets the dialog.

More