From 16faa2ca67db71617f808d9d0bbad8a0a04bfd41 Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 4 Apr 2016 12:33:20 +0200 Subject: modules: Rename gpg-agent to gnupg We do things such as placing gnupg into environment.systemPackages, so calling this just "programs.gpg-agent" doesn't fit that. Especially if we really want to have a way to specify configuration values in case I'm getting masochistic someday ;-) Signed-off-by: aszlig --- modules/programs/gnupg/agent-wrapper.c | 233 +++++++++++++++++++++ modules/programs/gnupg/default.nix | 174 ++++++++++++++++ modules/programs/gnupg/pinentry-wrapper.c | 281 ++++++++++++++++++++++++++ modules/programs/gpg-agent/agent-wrapper.c | 233 --------------------- modules/programs/gpg-agent/default.nix | 165 --------------- modules/programs/gpg-agent/pinentry-wrapper.c | 281 -------------------------- 6 files changed, 688 insertions(+), 679 deletions(-) create mode 100644 modules/programs/gnupg/agent-wrapper.c create mode 100644 modules/programs/gnupg/default.nix create mode 100644 modules/programs/gnupg/pinentry-wrapper.c delete mode 100644 modules/programs/gpg-agent/agent-wrapper.c delete mode 100644 modules/programs/gpg-agent/default.nix delete mode 100644 modules/programs/gpg-agent/pinentry-wrapper.c (limited to 'modules/programs') 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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); +} diff --git a/modules/programs/gnupg/default.nix b/modules/programs/gnupg/default.nix new file mode 100644 index 00000000..c6034f11 --- /dev/null +++ b/modules/programs/gnupg/default.nix @@ -0,0 +1,174 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.vuizvui.programs.gnupg; + + pinentryWrapper = pkgs.runCommand "pinentry-wrapper" { + pinentryProgram = cfg.agent.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 \ + -DLIBSYSTEMD=\"${pkgs.systemd}/lib/libsystemd.so\" \ + -DPINENTRY_WRAPPER=\"$pinentryWrapper\" \ + $(pkg-config --cflags libsystemd) -ldl \ + "${./agent-wrapper.c}" -o "$out" -fPIC + ''; + + agentSocketConfig = name: { + FileDescriptorName = name; + Service = "gpg-agent.service"; + SocketMode = "0600"; + DirectoryMode = "0700"; + }; + +in { + options.vuizvui.programs.gnupg = { + enable = mkEnableOption "support for GnuPG"; + + 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 and make available in + . + ''; + }; + + agent = { + enable = mkEnableOption "support for the GnuPG 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.gnupg"; + 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" "gnupg"]; + environment.systemPackages = [ cfg.package ]; + environment.variables.GNUPGHOME = "~/${cfg.homeDir}"; + }) + (mkIf (cfg.enable && cfg.agent.enable) { + 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.agent.scdaemon.enable + then "--scdaemon-program=${scdaemonRedirector}" + else "--disable-scdaemon") + "--no-detach" + "--daemon" + ] ++ optional cfg.agent.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.agent.enable && cfg.agent.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.agent.scdaemon.program}" + "--no-detach" + "--daemon" + ]; + }; + }) + (mkIf (cfg.enable && cfg.agent.enable && cfg.agent.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/gnupg/pinentry-wrapper.c b/modules/programs/gnupg/pinentry-wrapper.c new file mode 100644 index 00000000..12710760 --- /dev/null +++ b/modules/programs/gnupg/pinentry-wrapper.c @@ -0,0 +1,281 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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/agent-wrapper.c b/modules/programs/gpg-agent/agent-wrapper.c deleted file mode 100644 index 86e44c1a..00000000 --- a/modules/programs/gpg-agent/agent-wrapper.c +++ /dev/null @@ -1,233 +0,0 @@ -#define _GNU_SOURCE -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -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); -} diff --git a/modules/programs/gpg-agent/default.nix b/modules/programs/gpg-agent/default.nix deleted file mode 100644 index 139813d0..00000000 --- a/modules/programs/gpg-agent/default.nix +++ /dev/null @@ -1,165 +0,0 @@ -{ 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 \ - -DLIBSYSTEMD=\"${pkgs.systemd}/lib/libsystemd.so\" \ - -DPINENTRY_WRAPPER=\"$pinentryWrapper\" \ - $(pkg-config --cflags libsystemd) -ldl \ - "${./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 deleted file mode 100644 index 12710760..00000000 --- a/modules/programs/gpg-agent/pinentry-wrapper.c +++ /dev/null @@ -1,281 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -/* 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; -} -- cgit 1.4.1