about summary refs log tree commit diff
path: root/modules/programs/gnupg/agent-wrapper.c
diff options
context:
space:
mode:
Diffstat (limited to 'modules/programs/gnupg/agent-wrapper.c')
-rw-r--r--modules/programs/gnupg/agent-wrapper.c233
1 files changed, 233 insertions, 0 deletions
diff --git a/modules/programs/gnupg/agent-wrapper.c b/modules/programs/gnupg/agent-wrapper.c
new file mode 100644
index 00000000..86e44c1a
--- /dev/null
+++ b/modules/programs/gnupg/agent-wrapper.c
@@ -0,0 +1,233 @@
+#define _GNU_SOURCE
+#include <dlfcn.h>
+
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <malloc.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <systemd/sd-daemon.h>
+
+static int main_fd = 0;
+static int ssh_fd = 0;
+static int scdaemon_fd = 0;
+
+/* Get the systemd file descriptor for a particular socket file.
+ * Returns -1 if there is an error or -2 if it is an unnamed socket.
+ */
+static int get_sd_fd_for(const struct sockaddr_un *addr)
+{
+    if (main_fd == 0 && ssh_fd == 0 && scdaemon_fd == 0) {
+        int num_fds;
+        char **fdmap = NULL;
+        void *libsystemd = NULL;
+        int (*_sd_listen_fds_with_names)(int, char ***);
+
+        if ((libsystemd = dlopen(LIBSYSTEMD, RTLD_LAZY)) == NULL) {
+            fprintf(stderr, "dlopen %s\n", dlerror());
+            return -1;
+        }
+
+        _sd_listen_fds_with_names =
+            dlsym(libsystemd, "sd_listen_fds_with_names");
+
+        if (_sd_listen_fds_with_names == NULL) {
+            fprintf(stderr, "dlsym %s\n", dlerror());
+            return -1;
+        }
+
+        num_fds = _sd_listen_fds_with_names(0, &fdmap);
+
+        if (num_fds <= 0) {
+            fputs("No suitable file descriptors in LISTEN_FDS.\n", stderr);
+            if (num_fds == 0)
+                errno = EADDRNOTAVAIL;
+            return -1;
+        }
+
+        if (fdmap != NULL) {
+            for (int i = 0; i < num_fds; i++) {
+                if (strncmp(fdmap[i], "main", 5) == 0)
+                    main_fd = SD_LISTEN_FDS_START + i;
+                else if (strncmp(fdmap[i], "ssh", 4) == 0)
+                    ssh_fd = SD_LISTEN_FDS_START + i;
+                else if (strncmp(fdmap[i], "scdaemon", 9) == 0)
+                    scdaemon_fd = SD_LISTEN_FDS_START + i;
+                free(fdmap[i]);
+            }
+            free(fdmap);
+        }
+
+        if (dlclose(libsystemd) != 0)
+            return -1;
+    }
+
+    if (addr->sun_path == NULL || *(addr->sun_path) == 0)
+        return -2;
+
+    char *basename = strrchr(addr->sun_path, '/');
+    if (basename == NULL) {
+        fprintf(stderr, "Socket path %s is not absolute.\n",
+                addr->sun_path);
+        errno = EADDRNOTAVAIL;
+        return -1;
+    } else {
+        basename++;
+    }
+
+    if (strncmp(basename, "S.gpg-agent", 12) == 0) {
+        return main_fd;
+    } else if (strncmp(basename, "S.gpg-agent.ssh", 16) == 0) {
+        return ssh_fd;
+    } else if (strncmp(basename, "S.scdaemon", 11) == 0) {
+        return scdaemon_fd;
+    } else {
+        fprintf(stderr, "Socket path %s is unknown.\n", addr->sun_path);
+        errno = EADDRNOTAVAIL;
+        return -1;
+    }
+}
+
+/* Replace the systemd-provided socket FD with the one that is used by the
+ * agent, so that we can later look it up in our accept() wrapper.
+ */
+static void record_sockfd(int sysd_fd, int redir_fd)
+{
+    if (sysd_fd == main_fd)
+        main_fd = redir_fd;
+    else if (sysd_fd == ssh_fd)
+        ssh_fd = redir_fd;
+}
+
+/* systemd is already listening on that socket, so we don't need to. */
+int listen(int sockfd, int backlog)
+{
+    return 0;
+}
+
+/* The agent should already have called socket() before and we need to close the
+ * file descriptor that the socket() call has returned and replace it with the
+ * one provided by systemd.
+ */
+int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
+{
+    int new_fd;
+
+    new_fd = get_sd_fd_for((const struct sockaddr_un *)addr);
+
+    switch (new_fd) {
+        case -1: return -1;
+        /* Unnamed socket, just pretend everything is fine */
+        case -2: return 0;
+    }
+
+    if ((new_fd = get_sd_fd_for((const struct sockaddr_un *)addr)) == -1)
+        return -1;
+
+    fprintf(stderr, "bind: Redirecting FD %d to systemd-provided FD %d.\n",
+            sockfd, new_fd);
+
+    if (dup2(new_fd, sockfd) == -1)
+        return -1;
+    else
+        record_sockfd(new_fd, sockfd);
+
+    return 0;
+}
+
+/* Avoid forking for the first time so we can properly track the agent using a
+ * systemd service (without the need to set Type="forking").
+ */
+pid_t fork(void)
+{
+    static int first_fork = 1;
+
+    static pid_t (*_fork)(void) = NULL;
+    if (_fork == NULL)
+        _fork = dlsym(RTLD_NEXT, "fork");
+
+    /* Unset the LD_PRELOAD environment variable to make sure we don't propagate
+     * it down to things like the pinentry.
+     */
+    if (unsetenv("LD_PRELOAD") == -1)
+        return -1;
+
+    if (first_fork)
+        return first_fork = 0;
+
+    return _fork();
+}
+
+/* Get the PID of the client connected to the given socket FD. */
+static pid_t get_socket_pid(int sockfd)
+{
+    struct ucred pcred;
+    socklen_t pcred_len = sizeof(pcred);
+
+    if (getsockopt(sockfd, SOL_SOCKET, SO_PEERCRED, &pcred, &pcred_len) == -1)
+        return -1;
+
+    return pcred.pid;
+}
+
+static pid_t last_pid = 0;
+
+/* For the pinentry to work correctly with SSH, we need to record the process ID
+ * of the process communicating with the agent. That way we can get more
+ * information about the PTS/PTY/TTY the user is on and also know whether a
+ * DISPLAY is set for that process, because we will connect the pinentry's TTY
+ * to the TTY of the process on the other end of the socket.
+ */
+int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
+{
+    int retval;
+
+    static int (*_accept)(int, struct sockaddr *, socklen_t *) = NULL;
+    if (_accept == NULL)
+        _accept = dlsym(RTLD_NEXT, "accept");
+
+    retval = _accept(sockfd, addr, addrlen);
+
+    last_pid = 0;
+
+    if (retval != -1 && ssh_fd != 0 && sockfd == ssh_fd) {
+        pid_t client_pid = get_socket_pid(retval);
+        if (client_pid == -1) {
+            close(retval);
+            return -1;
+        }
+        last_pid = client_pid;
+        fprintf(stderr, "Socket endpoint PID for accepted socket %d is %d.\n",
+                retval, client_pid);
+    }
+
+    return retval;
+}
+
+/* Wrap the execv() that calls the pinentry program to include a special
+ * _CLIENT_PID environment variable, which contains the PID we gathered during
+ * accept(). Note that this is potentially racy if we have a lot of concurrent
+ * connections, but the worst that could happen is that we end up having a
+ * pinentry running on the wrong TTY/display.
+ */
+int execv(const char *path, char *const argv[])
+{
+    static int (*_execv)(const char *, char *const[]) = NULL;
+    if (_execv == NULL)
+        _execv = dlsym(RTLD_NEXT, "execv");
+
+    if (last_pid != 0 &&
+        strncmp(path, PINENTRY_WRAPPER, sizeof(PINENTRY_WRAPPER) + 1) == 0) {
+        char env_var[40];
+        if (snprintf(env_var, 40, "_CLIENT_PID=%d", last_pid) < 0)
+            return -1;
+        if (putenv(env_var) < 0)
+            return -1;
+    }
+
+    last_pid = 0;
+    return _execv(path, argv);
+}