Line data Source code
1 : // SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief Implements SshAuthSock; a probed socket is adopted only after
6 : * `ssh-add -l` answers on it (isSshAgentReachable()).
7 : */
8 : #include "sshauthsock.h"
9 : #include "executor.h"
10 : #include <QFileInfo>
11 : #include <QProcessEnvironment>
12 : #ifndef Q_OS_WIN
13 : #include <sys/stat.h>
14 : #endif
15 :
16 : #include "qtpasslogging.h"
17 :
18 : namespace {
19 : /**
20 : * @brief Verify that a candidate SSH_AUTH_SOCK socket has a live agent
21 : * listening, before letting QtPass switch the environment to it.
22 : *
23 : * Why: a user may have a working external SSH agent (OpenSSH ssh-agent,
24 : * KeePassXC / 1Password / yubikey-agent / gnome-keyring) configured in
25 : * their shell, but its env didn't propagate to the GUI launcher. If we
26 : * blindly adopt whatever `gpgconf --list-dirs agent-ssh-socket` reports —
27 : * even when gpg-agent isn't running with SSH support, or has no keys —
28 : * we'd be silently switching them away from their real agent.
29 : *
30 : * Approach: spawn `ssh-add -l` with SSH_AUTH_SOCK pointed at the candidate
31 : * (via Executor's env-aware overload, so the parent env isn't disturbed).
32 : * Treat exit codes 0 (keys present) and 1 (agent alive but key list empty —
33 : * legitimate for YubiKey-backed setups that enumerate on tap) as
34 : * "reachable". Anything else, including ssh-add not being on PATH, means
35 : * we don't adopt the candidate.
36 : *
37 : * @param candidate Path to validate.
38 : * @return true if the socket has a reachable agent; false otherwise.
39 : */
40 2 : auto isSshAgentReachable(const QString &candidate) -> bool {
41 2 : if (candidate.isEmpty()) {
42 : return false;
43 : }
44 : // Build a minimal env that overrides SSH_AUTH_SOCK without polluting the
45 : // parent process. systemEnvironment() captures whatever was set when
46 : // QtPass started, then we override the one var of interest.
47 2 : QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
48 4 : env.insert(QStringLiteral("SSH_AUTH_SOCK"), candidate);
49 2 : QString out;
50 2 : QString err;
51 6 : const int exitCode = Executor::executeBlocking(
52 4 : env, QStringLiteral("ssh-add"), {QStringLiteral("-l")}, &out, &err);
53 : // OpenSSH ssh-add(1) exit codes: 0 = success / has keys, 1 = no
54 : // identities present, 2 = couldn't open a connection. Anything else is
55 : // also treated as unreachable (e.g. ssh-add not on PATH).
56 2 : return exitCode == 0 || exitCode == 1;
57 2 : }
58 : /// Ask @p program for a socket and adopt it when an agent answers there;
59 : /// @p source names it in the log. Whether a socket was adopted.
60 2 : auto adoptSocketFrom(const QString &program, const QStringList &args,
61 : const char *source) -> bool {
62 2 : QString out;
63 2 : QString err;
64 2 : if (Executor::executeBlocking(program, args, &out, &err) != 0) {
65 : return false;
66 : }
67 : const QString socket = out.trimmed();
68 2 : if (socket.isEmpty()) {
69 : return false;
70 : }
71 2 : if (!isSshAgentReachable(socket)) {
72 0 : qCDebug(lcQtPass) << "SshAuthSock::initialise():" << source << "reported"
73 0 : << socket << "but ssh-add -l rejected it; not adopting";
74 0 : return false;
75 : }
76 2 : qputenv("SSH_AUTH_SOCK", socket.toUtf8());
77 2 : qCDebug(lcQtPass) << "SshAuthSock::initialise(): set from" << source << ":"
78 0 : << socket;
79 2 : return true;
80 : }
81 :
82 : } // namespace
83 :
84 7 : auto SshAuthSock::overrideStatus(const QString &path) -> OverrideStatus {
85 7 : const QFileInfo fi(path);
86 7 : if (!fi.exists()) {
87 : return OverrideStatus::DoesNotExist;
88 : }
89 5 : if (!fi.isReadable()) {
90 : return OverrideStatus::NotReadable;
91 : }
92 : #ifdef Q_OS_UNIX
93 : // S_ISSOCK isn't exposed by QFileInfo; go through stat(2). On Windows
94 : // ssh-agent uses a named pipe (\\.\pipe\openssh-ssh-agent.<sid>) and the
95 : // socket check would always fail, so skip it there — exists + readable is
96 : // the best we can do.
97 3 : struct stat st{};
98 6 : if (::stat(path.toLocal8Bit().constData(), &st) != 0 ||
99 3 : !S_ISSOCK(st.st_mode)) {
100 2 : return OverrideStatus::NotUnixDomainSocket;
101 : }
102 : #endif
103 : return OverrideStatus::Valid;
104 7 : }
105 :
106 : // The rules are in the header. All probes go through
107 : // Executor::executeBlocking with no timeout or cancel flag, so each one
108 : // waits for its child (gpgconf, ssh-add -l) however long that takes. Any
109 : // failure is silently absorbed (the user just doesn't get the auto-fix).
110 5 : void SshAuthSock::initialise(const QString &override) {
111 : // Honour any value already in the environment — terminal launches,
112 : // explicit `.desktop` Exec= overrides, and parent-process exports must win.
113 10 : if (!qgetenv("SSH_AUTH_SOCK").isEmpty()) {
114 : return;
115 : }
116 :
117 : // Manual override from settings takes precedence over auto-probe.
118 4 : if (!override.isEmpty()) {
119 2 : qputenv("SSH_AUTH_SOCK", override.toUtf8());
120 2 : qCDebug(lcQtPass)
121 0 : << "SshAuthSock::initialise(): set from settings override:" << override;
122 2 : return;
123 : }
124 :
125 : // Auto-probe via gpgconf (canonical for gpg-agent's SSH support).
126 8 : if (adoptSocketFrom(
127 2 : QStringLiteral("gpgconf"),
128 2 : {QStringLiteral("--list-dirs"), QStringLiteral("agent-ssh-socket")},
129 : "gpgconf")) {
130 : return;
131 : }
132 : #ifdef Q_OS_MACOS
133 : // On macOS, GUI-launched apps may have SSH_AUTH_SOCK in launchd's
134 : // per-session environment but not in the inherited process env.
135 : adoptSocketFrom(QStringLiteral("launchctl"),
136 : {QStringLiteral("getenv"), QStringLiteral("SSH_AUTH_SOCK")},
137 : "launchctl");
138 : #endif
139 : }
|