about summary refs log tree commit diff
path: root/modules/programs
diff options
context:
space:
mode:
Diffstat (limited to 'modules/programs')
-rw-r--r--modules/programs/gpg-agent/agent-wrapper.c214
-rw-r--r--modules/programs/gpg-agent/default.nix164
-rw-r--r--modules/programs/gpg-agent/pinentry-wrapper.c281
-rw-r--r--modules/programs/gpg-agent/test.nix11
4 files changed, 670 insertions, 0 deletions
diff --git a/modules/programs/gpg-agent/agent-wrapper.c b/modules/programs/gpg-agent/agent-wrapper.c
new file mode 100644
index 00000000..e969e85b
--- /dev/null
+++ b/modules/programs/gpg-agent/agent-wrapper.c
@@ -0,0 +1,214 @@
+#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>
+
+int main_fd = 0;
+int ssh_fd = 0;
+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.
+ */
+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;
+        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 (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.
+ */
+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").
+ */
+int first_fork = 1;
+
+pid_t fork(void)
+{
+    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. */
+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;
+}
+
+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);
+}
diff --git a/modules/programs/gpg-agent/default.nix b/modules/programs/gpg-agent/default.nix
new file mode 100644
index 00000000..81a113f7
--- /dev/null
+++ b/modules/programs/gpg-agent/default.nix
@@ -0,0 +1,164 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.programs.gpg-agent;
+
+  pinentryWrapper = pkgs.runCommand "pinentry-wrapper" {
+    pinentryProgram = cfg.pinentry.program;
+  } ''
+    cc -Wall -std=gnu11 -DPINENTRY_PROGRAM=\"$pinentryProgram\" \
+      "${./pinentry-wrapper.c}" -o "$out"
+  '';
+
+  scdaemonRedirector = pkgs.writeScript "scdaemon-redirector" ''
+    #!${pkgs.stdenv.shell}
+    exec "${pkgs.socat}/bin/socat" - \
+      UNIX-CONNECT:"$HOME/${cfg.homeDir}/S.scdaemon"
+  '';
+
+  agentWrapper = pkgs.runCommand "gpg-agent-wrapper" {
+    buildInputs = with pkgs; [ pkgconfig systemd ];
+    inherit pinentryWrapper;
+  } ''
+    cc -Wall -shared -std=c11 \
+      $(pkg-config --cflags --libs libsystemd) -ldl \
+      -DPINENTRY_WRAPPER=\"$pinentryWrapper\" \
+      "${./agent-wrapper.c}" -o "$out" -fPIC
+  '';
+
+  agentSocketConfig = name: {
+    FileDescriptorName = name;
+    Service = "gpg-agent.service";
+    SocketMode = "0600";
+    DirectoryMode = "0700";
+  };
+
+in {
+  options.vuizvui.programs.gpg-agent = {
+    enable = mkEnableOption "support for GnuPG agent";
+
+    homeDir = mkOption {
+      type = types.addCheck types.str (d: builtins.substring 0 1 d != "/");
+      default = ".gnupg";
+      description = ''
+        The directory where GnuPG keeps its state files and configuration files,
+        relative to the user's home directory.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.gnupg;
+      defaultText = "pkgs.gnupg";
+      example = literalExample "pkgs.gnupg21";
+      description = "The GnuPG package to use for running the agent.";
+    };
+
+    pinentry.program = mkOption {
+      type = types.path;
+      default = "${pkgs.pinentry}/bin/pinentry";
+      defaultText = "\${pkgs.pinentry}/bin/pinentry";
+      example = literalExample "\${pkgs.pinentry_qt5}/bin/pinentry";
+      description = "The pinentry program to use to ask for passphrases.";
+    };
+
+    sshSupport = mkEnableOption "GnuPG agent support for SSH";
+
+    scdaemon = {
+      enable = mkEnableOption "GnuPG agent with Smartcard daemon";
+
+      program = mkOption {
+        type = types.path;
+        default = "${cfg.package}/libexec/scdaemon";
+        defaultText = let
+          configPath = "config.vuizvui.programs.gpg-agent";
+        in "\${${configPath}.package}/libexec/scdaemon";
+        example = literalExample "\${pkgs.my_shiny_scdaemon}/bin/scdaemon";
+        description = "The program to use for the Smartcard daemon";
+      };
+    };
+  };
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      vuizvui.requiresTests = singleton ["vuizvui" "programs" "gpg-agent"];
+      environment.systemPackages = [ cfg.package ];
+
+      systemd.user.services.gpg-agent = {
+        description = "GnuPG Agent";
+        environment.LD_PRELOAD = agentWrapper;
+        environment.GNUPGHOME = "~/${cfg.homeDir}";
+
+        serviceConfig.ExecStart = toString ([
+          "${cfg.package}/bin/gpg-agent"
+          "--pinentry-program=${pinentryWrapper}"
+          (if cfg.scdaemon.enable
+           then "--scdaemon-program=${scdaemonRedirector}"
+           else "--disable-scdaemon")
+          "--no-detach"
+          "--daemon"
+        ] ++ optional cfg.sshSupport "--enable-ssh-support");
+
+        serviceConfig.ExecReload = toString [
+          "${cfg.package}/bin/gpg-connect-agent"
+          "RELOADAGENT"
+          "/bye"
+        ];
+      };
+
+      systemd.user.sockets.gpg-agent-main = {
+        wantedBy = [ "sockets.target" ];
+        description = "Main Socket For GnuPG Agent";
+        listenStreams = [ "%h/${cfg.homeDir}/S.gpg-agent" ];
+        socketConfig = agentSocketConfig "main";
+      };
+    })
+    (mkIf (cfg.enable && cfg.scdaemon.enable) {
+      systemd.user.sockets.gnupg-scdaemon = {
+        wantedBy = [ "sockets.target" ];
+        description = "GnuPG Smartcard Daemon Socket";
+        listenStreams = [ "%h/${cfg.homeDir}/S.scdaemon" ];
+        socketConfig = {
+          FileDescriptorName = "scdaemon";
+          SocketMode = "0600";
+          DirectoryMode = "0700";
+        };
+      };
+
+      systemd.user.services.gnupg-scdaemon = {
+        description = "GnuPG Smartcard Daemon";
+        environment.LD_PRELOAD = agentWrapper;
+        environment.GNUPGHOME = "~/${cfg.homeDir}";
+
+        serviceConfig.ExecStart = toString [
+          "${cfg.scdaemon.program}"
+          "--no-detach"
+          "--daemon"
+        ];
+      };
+    })
+    (mkIf (cfg.enable && cfg.sshSupport) {
+      environment.variables.SSH_AUTH_SOCK =
+        "$HOME/${cfg.homeDir}/S.gpg-agent.ssh";
+
+      systemd.user.sockets.gpg-agent-ssh = {
+        wantedBy = [ "sockets.target" ];
+        description = "SSH Socket For GnuPG Agent";
+        listenStreams = [ "%h/${cfg.homeDir}/S.gpg-agent.ssh" ];
+        socketConfig = agentSocketConfig "ssh";
+      };
+
+      assertions = singleton {
+        assertion = !config.programs.ssh.startAgent;
+        message = toString [
+          "You cannot use the GnuPG agent with SSH support in addition to the"
+          "SSH agent, please either disable"
+          "`vuizvui.programs.gpg-agent.sshSupport' or disable"
+          "`programs.ssh.startAgent'."
+        ];
+      };
+    })
+  ];
+}
diff --git a/modules/programs/gpg-agent/pinentry-wrapper.c b/modules/programs/gpg-agent/pinentry-wrapper.c
new file mode 100644
index 00000000..12710760
--- /dev/null
+++ b/modules/programs/gpg-agent/pinentry-wrapper.c
@@ -0,0 +1,281 @@
+#include <sys/types.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+/* Get the terminal path of the given PID and FD using the /proc file system. */
+char *get_terminal(pid_t pid, int fd)
+{
+    int term, is_term;
+    ssize_t linklen;
+    char fd_path[50];
+    char term_path[100];
+    struct stat st;
+
+    if (snprintf(fd_path, 50, "/proc/%d/fd/%d", pid, fd) < 0) {
+        perror("snprintf proc fd path");
+        return NULL;
+    }
+
+    if (lstat(fd_path, &st) == -1)
+        return NULL;
+
+    if (!S_ISLNK(st.st_mode))
+        return NULL;
+
+    if ((linklen = readlink(fd_path, term_path, sizeof term_path)) == -1) {
+        perror("readlink term path");
+        return NULL;
+    }
+
+    term_path[linklen] = 0;
+
+    if ((term = open(term_path, O_RDONLY | O_NOCTTY)) == -1)
+        return NULL;
+
+    is_term = isatty(term);
+
+    if (close(term) == -1) {
+        perror("close client tty");
+        return NULL;
+    }
+
+    if (!is_term)
+        return NULL;
+
+    return strdup(term_path);
+}
+
+/* Probes FD 0, 1 and 2 for a connected terminal device and return an allocated
+ * string pointing to the filename.
+ */
+char *detect_terminal(pid_t pid)
+{
+    char *term;
+
+    for (int i = 0; i < 3; ++i) {
+        term = get_terminal(pid, i);
+        if (term == NULL)
+            continue;
+
+        return term;
+    }
+
+    return NULL;
+}
+
+/* Fetch the info from /proc/PID/environ and retorn it as an array. */
+char **fetch_environ(pid_t pid)
+{
+    char environ_path[50], **result = NULL;
+    char buf[2048], *envbuf, *environ = NULL;
+    size_t chunklen, envlen = 0;
+    int env_fd;
+
+    if (snprintf(environ_path, 50, "/proc/%d/environ", pid) < 0) {
+        perror("snprintf proc environ path");
+        return NULL;
+    }
+
+    if ((env_fd = open(environ_path, O_RDONLY)) == -1) {
+        perror("open proc environ");
+        return NULL;
+    }
+
+    while ((chunklen = read(env_fd, buf, sizeof buf)) > 0) {
+        if (environ == NULL) {
+            if ((environ = malloc(envlen + chunklen + 1)) == NULL) {
+                perror("malloc proc environ");
+                return NULL;
+            }
+        } else {
+            if ((environ = realloc(environ, envlen + chunklen + 1)) == NULL) {
+                perror("realloc proc environ");
+                free(environ);
+                return NULL;
+            }
+        }
+        memcpy(environ + envlen, buf, chunklen);
+        envlen += chunklen;
+        environ[envlen + 1] = 0;
+        if (chunklen < sizeof buf)
+            break;
+    }
+
+    if (close(env_fd) == -1) {
+        perror("close proc environ");
+        free(environ);
+        return NULL;
+    }
+
+    envbuf = environ;
+
+    if ((result = malloc(sizeof(char*))) == NULL) {
+        perror("malloc environ array");
+        free(environ);
+        return NULL;
+    }
+    result[0] = NULL;
+
+    for (int i = 0; envbuf - environ < envlen; ++i) {
+        if ((result = realloc(result, sizeof(char*) * (i + 2))) == NULL) {
+            perror("realloc environ array");
+            free(environ);
+            free(result);
+            return NULL;
+        }
+
+        result[i] = strndup(envbuf, envlen - (envbuf - environ));
+        result[i + 1] = NULL;
+        envbuf += strlen(envbuf) + 1;
+    }
+
+    free(environ);
+    return result;
+}
+
+void free_environ(char **environ)
+{
+    char **tmp = environ;
+    if (environ == NULL) return;
+    do free(*tmp);
+    while (*(++tmp) != NULL);
+    free(environ);
+    environ = NULL;
+}
+
+struct proc_info {
+    char **environ;
+    char *term;
+};
+
+/* Gather information for the given process ID, like environment or connected
+ * terminals.
+ */
+struct proc_info *open_proc_info(pid_t pid)
+{
+    struct proc_info *pi = NULL;
+
+    if ((pi = malloc(sizeof(struct proc_info *))) == NULL) {
+        perror("malloc proc_info");
+        return NULL;
+    }
+
+    pi->term = detect_terminal(pid);
+    if ((pi->environ = fetch_environ(pid)) == NULL) {
+        free(pi->term);
+        free(pi);
+        return NULL;
+    }
+
+    return pi;
+}
+
+void close_proc_info(struct proc_info *pi)
+{
+    if (pi->term != NULL) free(pi->term);
+    free_environ(pi->environ);
+    free(pi);
+}
+
+/* Fetch an environment variable from the proc_info structure similar to
+ * getenv() but for remote PIDs.
+ */
+char *proc_info_getenv(struct proc_info *pi, const char *name)
+{
+    char **tmp = pi->environ;
+    size_t namelen = strlen(name);
+    do {
+        if (strncmp(*tmp, name, namelen) == 0 &&
+            *(*tmp + namelen) == '=') {
+            return strdup(*tmp + namelen + 1);
+        }
+    } while (*(++tmp) != NULL);
+    return NULL;
+}
+
+#define MAYBE_EXPAND_ARGV(opt, value) \
+    if ((tmp = value) != NULL) { \
+        new_argv = realloc(new_argv, sizeof(char*) * (new_argc + 3)); \
+        if (new_argv == NULL) { \
+            perror("realloc new argv"); \
+            return EXIT_FAILURE; \
+        } \
+        new_argv[new_argc + 0] = "--" opt; \
+        new_argv[new_argc + 1] = tmp; \
+        new_argv[new_argc + 2] = NULL; \
+        new_argc += 2; \
+    }
+
+/* This is our program main routine whenever we get a _CLIENT_PID environment
+ * variable.
+ */
+int wrap(struct proc_info *pi, int argc, char **argv)
+{
+    char *tmp, **new_argv;
+    int new_argc = 1;
+
+    if ((new_argv = malloc(sizeof(char*) * 2)) == NULL) {
+        perror("malloc new argv");
+        return EXIT_FAILURE;
+    }
+
+    new_argv[0] = PINENTRY_PROGRAM;
+    new_argv[1] = NULL;
+
+    MAYBE_EXPAND_ARGV("display", proc_info_getenv(pi, "DISPLAY"));
+    MAYBE_EXPAND_ARGV("ttyname", strdup(pi->term));
+    MAYBE_EXPAND_ARGV("ttytype", proc_info_getenv(pi, "TERM"));
+    MAYBE_EXPAND_ARGV("lc-ctype", proc_info_getenv(pi, "LC_CTYPE"));
+    MAYBE_EXPAND_ARGV("lc-messages", proc_info_getenv(pi, "LC_MESSAGES"));
+
+    close_proc_info(pi);
+
+    /* No DISPLAY/TTY found, so use the arguments provided by the agent. */
+    if (new_argc == 1) {
+        free(new_argv);
+        new_argv = argv;
+    }
+
+    /* Make sure we don't have DISPLAY already in our environment to avoid
+     * starting a pinentry on X while the user is connected via SSH for example.
+     */
+    if (unsetenv("DISPLAY") == -1)
+        return EXIT_FAILURE;
+
+    if (execv(PINENTRY_PROGRAM, new_argv) == -1) {
+        perror("execv real pinentry");
+        return EXIT_FAILURE;
+    }
+
+    /* Not reached because the process should be substituted in execve(). */
+    return EXIT_SUCCESS;
+}
+
+int main(int argc, char **argv)
+{
+    const char *pidstr;
+    struct proc_info *pi = NULL;
+
+    if ((pidstr = getenv("_CLIENT_PID")) != NULL) {
+        if ((pi = open_proc_info(atoi(pidstr))) == NULL)
+            fprintf(stderr, "Client PID %d has vanished before we could"
+                    " retrieve /proc information.\n", atoi(pidstr));
+        else
+            return wrap(pi, argc, argv);
+    }
+
+    argv[0] = PINENTRY_PROGRAM;
+
+    if (execv(PINENTRY_PROGRAM, argv) == -1) {
+        perror("execv real pinentry");
+        return EXIT_FAILURE;
+    }
+
+    /* Not reached because the process should be substituted in execve(). */
+    return EXIT_SUCCESS;
+}
diff --git a/modules/programs/gpg-agent/test.nix b/modules/programs/gpg-agent/test.nix
new file mode 100644
index 00000000..ecfa2737
--- /dev/null
+++ b/modules/programs/gpg-agent/test.nix
@@ -0,0 +1,11 @@
+let
+  pkgs = import <nixpkgs> {};
+in
+
+  pkgs.runCommand "gpg-agent-wrapper" {
+    buildInputs = with pkgs; [ pkgconfig systemd ];
+  } ''
+    cc -Wall -shared -std=c11 \
+      $(pkg-config --cflags --libs libsystemd) \
+      "${./agent-wrapper.c}" -o "$out" -ldl -fPIC
+  ''