用 Zig 编写的 GTK4 ssh-askpass
Gentoo 软件树中的每一个 ssh-askpass 都会引入 X11 或整套 KDE 组件,所以我用 Zig 0.16 和 GTK4 自己写了一个,采用手写绑定,让 X 彻底远离构建过程。

我在笔记本上运行加固版 Gentoo,而且大多数时候我根本用不到 ssh-askpass,因为大多数系统我用的都是 -sk 密钥。但有一类情况确实需要它:某个程序需要常规 ED25519 密钥的 SSH 口令短语,却没有终端可以读取输入。最常见的就是 go get,或者泛指 go 工具链,在无 TTY 的构建过程中通过 SSH 拉取私有模块。OpenSSH 无法在管道上提示输入,于是它会运行 SSH_ASKPASS 所指向的程序,把口令短语提示放到一个窗口里。多年来我什么都没装,只能想办法绕过这些场景。主要原因就是 Gentoo 的 Portage 能提供的选项:
~ 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 ]这五个每一个都有至少一处让我不想忍受的不便。我的系统全局启用了 -X USE 标志,所以任何需要 X11 的东西我都不用再看第二眼。五个之中,kde-plasma/ksshaskpass 是唯一完全不依赖 X11 的,本应是显而易见的选择,但问题在于它会带来一大堆别的东西。作为一个Sway 用户,我不想为了偶尔输入一次口令短语就在机器上装整套 KDE,而安装 ksshaskpass 恰恰就会拉入这些东西:
~ # 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,它直接需要 X。此外它还会拉入几个 KDE 框架包以及一个启用 X 支持构建的 Qt,这与我系统上已经以 -X 编译的 dev-qt/qtbase 冲突,于是 Portage 在 slot 冲突处停下:
~ # 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)"
(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)"
The following USE changes are necessary to proceed:
# required by lxqt-base/liblxqt-2.4.0::gentoo
>=kde-frameworks/kwindowsystem-6.27.0 X
# required by kde-frameworks/kwindowsystem-6.27.0::gentoo[X]
>=dev-qt/qtbase-6.11.1 X
# required by dev-qt/qtbase-6.11.1::gentoo[gui]
>=x11-libs/libxkbcommon-1.13.2 Xnet-misc/ssh-askpass-fullscreen 同样需要 X,这次是通过 GTK2 和一个启用 X 支持构建的 Cairo:
~ # 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:
# 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 从名字上看就是 X11 的,没什么意外,而且它还需要古老的 imake 构建系统,即 x11-misc/xorg-cf-files 和 x11-misc/imake,否则根本无法编译:
~ # 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]最后剩下 net-misc/gnome-ssh-askpass。乍一看它似乎是唯一完全不需要 X 的选项,但事实证明并非如此。它确实需要 X11,而且 ebuild 在这方面看起来是坏的。构建过程调用 pkg-config --libs gtk+-3.0 x11,源码还包含了 gdk/gdkx.h 这个仅限 X 的 GDK 头文件,所以在没有 X 编译的系统上它会构建失败:
~ # 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到这里我放弃了对现成软件包的选择。即便撇开 X11 的问题不谈,这些工具最多也只用到 GTK2 或 GTK3。不过恰好我早就想用 GTK4 做点东西了,所以与其修补这些大多本来就是 C 写的实现,我不如用 Zig 0.16 和 GTK4 自己写了一个,并命名为 ssh-askpass-zigtk。
避开 X11
这些 GTK 工具在我的系统上出问题的原因在于头文件。调用 GTK 的标准方式会包含 GTK4 头文件,后者会引入 GDK,而 GDK 在大多数安装中仍然附带 gdk/gdkx.h,所以不管你愿不愿意,一个 X11 头文件都会被带进来。Zig 的 @cImport,也就是调用 C 库最显然的方式,也会同样如此,因为它引入的正是那些头文件。因此 ssh-askpass-zigtk 不 @cImport 任何东西。src/gtk.zig 以普通 extern 原型的方式手动声明了程序所调用的三十来个 GTK 和 GLib 函数:
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;这个文件里没有任何符号来自 gdk/gdkx.h 或 X11/Xlib.h,所以编译器永远看不到 X 头文件,二进制程序可以针对未启用 X11 编译的 GTK4 构建并运行。它需要的唯一一个与 X 相关的值,即 Escape 键的 keysym,是直接硬编码为 0xff1b,而不是从 gdk/gdkkeysyms.h 获取的。
GTK 构建在 GObject 之上,后者通过把父结构体作为子结构体的第一个成员来实现单继承,因此窗口、盒子、标签、密码输入框和按钮在 ABI 边界上都与 GtkWidget * 布局兼容。在 Zig 这边,一个 opaque {} 类型就代表了它们全部,每个控件函数都接收并返回同一个 *Widget,无需用一层包装类型体系去模拟 C ABI 早已抹平的东西。
不涉及 GTK 的部分,即把 SSH_ASKPASS_PROMPT 映射为对话框类型以及解析 GNOME_SSH_ASKPASS_*_COLOR 变量的逻辑,放在 src/root.zig 中并带有单元测试,因此它们可以在 zig build test 下运行,不需要显示器,也完全不需要 GTK。重新着色通过一个小的 CSS provider 实现,因为 GTK4 移除了 gtk_widget_modify_fg 和 _bg。
交叉编译
由于绑定是手写的 extern 声明,构建过程中不会进入任何 GTK 头文件,Zig 可以在没有目标平台 GTK4 工具链的情况下为任意 Linux 架构交叉编译该二进制程序。链接时唯一缺的就是 GTK4 共享库本身,-Dgtk-stub 可以解决这个问题:它构建一个极小的 stub libgtk-4.so.1,其导出符号全都是空操作,将可执行文件链接到它上面,然后让目标系统的真正 GTK4 在运行时完成解析。发布工作流利用这一点在一台机器上产出 x86_64、aarch64、armv7、riscv64、powerpc64le、i386、loongarch64 和 s390x 的二进制程序,而这台机器上并没有为其他七个架构安装 GTK4。
注意:ssh-askpass-zigtk 不像其他 askpass 实现通常会做的那样抓取键盘。GTK3 版本的辅助程序会调用 gdk_seat_grab,以防其他客户端在你输入时读取口令短语,但据我所见,GTK4 移除了该接口,而且我认为 Wayland 根本不允许客户端抓取键盘,所以在没有 X11 的情况下没有可移植的做法。因此 GNOME_SSH_ASKPASS_GRAB_SERVER 和 GNOME_SSH_ASKPASS_GRAB_POINTER 变量也不起作用。
代码托管在 tty.fail 上,并镜像到 GitHub,每个打标签的发布都会提供按架构划分的预编译 Linux 二进制程序。要使用它,把二进制文件放到 PATH 中的某个位置,并将 SSH_ASKPASS 指向它。对于终端用户来说,就是在 ~/.profile 或 shell 的启动文件中加两行(例如我的Zsh 同好们就是 ~/.zshrc):
export SSH_ASKPASS=~/.local/bin/ssh-askpass-zigtk
export SSH_ASKPASS_REQUIRE=prefer从 OpenSSH 8.4 开始,SSH_ASKPASS_REQUIRE=prefer 会让 OpenSSH 即使在有终端可用的情况下也使用对话框,只要存在图形会话即可。在 systemd 用户会话中,同样这两个变量要以纯 KEY=VALUE 行的形式、使用绝对路径放入 ~/.config/environment.d/ssh-askpass.conf,因为该文件既不展开 ~ 也不运行 shell。注销再重新登录之后,下一个需要口令短语而没有终端的 ssh-add、git pull 或 go get 就会弹出对话框。
随机一篇博客