用 Zig 打造的 GTK4 ssh-askpass
在 Gentoo 套件庫中的每一個 ssh-askpass 都會引入 X11 或整套 KDE,所以我用 Zig 0.16 與 GTK4 自己寫了一個,並以手寫綁定讓 X 完全不進入建置流程。

我的筆電上跑的是 強化版 Gentoo,大多數時候我根本不會用到 ssh-askpass,因為多數系統我都是使用 -sk 金鑰。但有一類情況還是需要它,那就是當某個程式需要為一般的 ED25519 金鑰取得密碼,卻沒有終端機可供讀取時。最典型的例子就是 go get,或是更廣義的 go 工具鏈,在沒有 TTY 的建置過程中透過 SSH 抓取私有模組。OpenSSH 無法在管線(pipe)上提示輸入,因此會執行 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 flag,因此任何需要 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>
| ^~~~~~~~~~~~\ncompilation 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 中並附有單元測試,因此可以在完全沒有顯示器、也沒有 GTK 的情況下透過 zig build test 執行。重新上色的部分是透過小型的 CSS provider 完成,因為 GTK4 已經移除了 gtk_widget_modify_fg 與 _bg。
交叉編譯
由於綁定是以手寫的 extern 形式撰寫,且沒有任何 GTK 標頭檔進入建置流程,Zig 可以在不需要為目標平台準備 GTK4 工具鏈的情況下,為任何 Linux 架構交叉編譯二進位檔。在連結時期唯一缺少的是 GTK4 共享函式庫本身,而 -Dgtk-stub 正好解決了這點,它會建置一個小型的 stub libgtk-4.so.1,其匯出的符號全都是空操作(no-ops),讓執行檔先與之連結,再在執行時期讓目標系統上真正的 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=preferSSH_ASKPASS_REQUIRE=prefer 從 OpenSSH 8.4 開始提供,它會告訴 OpenSSH 只要有圖形工作階段存在,即使有終端機可用,也優先使用對話框。在 systemd 使用者工作階段中,同樣的兩個變數要以單純的 KEY=VALUE 形式、並使用絕對路徑,寫入 ~/.config/environment.d/ssh-askpass.conf,因為該檔案既不會展開 ~,也不會執行 shell。登出後再重新登入,下一次需要密碼但沒有終端機的 ssh-add、git pull 或 go get 就會跳出對話框。
隨機一篇部落格