diff options
-rw-r--r-- | modules/module-list.nix | 1 | ||||
-rw-r--r-- | modules/programs/gpg-agent/agent-wrapper.c | 214 | ||||
-rw-r--r-- | modules/programs/gpg-agent/default.nix | 164 | ||||
-rw-r--r-- | modules/programs/gpg-agent/pinentry-wrapper.c | 281 | ||||
-rw-r--r-- | modules/programs/gpg-agent/test.nix | 11 | ||||
-rw-r--r-- | tests/default.nix | 3 | ||||
-rw-r--r-- | tests/programs/gpg-agent/default.nix | 127 | ||||
-rw-r--r-- | tests/programs/gpg-agent/snakeoil.asc | 59 |
8 files changed, 860 insertions, 0 deletions
diff --git a/modules/module-list.nix b/modules/module-list.nix index 9935021f..7a489c55 100644 --- a/modules/module-list.nix +++ b/modules/module-list.nix @@ -5,6 +5,7 @@ ./hardware/thinkpad.nix ./profiles/common.nix ./profiles/tests.nix + ./programs/gpg-agent ./services/multipath-vpn.nix ./services/postfix ./services/starbound.nix 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 + '' diff --git a/tests/default.nix b/tests/default.nix index 54b130a3..29fdb973 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -12,6 +12,9 @@ in { games = { starbound = callTest ./games/starbound.nix; }; + programs = { + gpg-agent = callTest ./programs/gpg-agent; + }; richi235 = { # Currently broken #multipath-vpn = callTest ./richi235/multipath-vpn.nix; diff --git a/tests/programs/gpg-agent/default.nix b/tests/programs/gpg-agent/default.nix new file mode 100644 index 00000000..d10fdbfe --- /dev/null +++ b/tests/programs/gpg-agent/default.nix @@ -0,0 +1,127 @@ +{ pkgs, ... }: + +let + mkExpect = expectScript: script: pkgs.writeScript "test-gnupg-cli" '' + #!${pkgs.expect}/bin/expect -f + set timeout 20 + spawn ${pkgs.writeScript "cli-testscript.sh" '' + #!${pkgs.stdenv.shell} -ex + ${script} + ''} + ${expectScript} + set ret [wait] + exit [lindex $ret 3] + ''; + + cliTestWithPassphrase = mkExpect '' + expect -regexp ---+.*Please.enter + send supersecret\r + ''; + + cliTest = mkExpect ""; + +in { + name = "gpg-agent"; + + enableOCR = true; + + machine = { lib, ... }: { + imports = map (what: + "${import ../../../nixpkgs-path.nix}/nixos/tests/common/${what}.nix" + ) [ "user-account" "x11" ]; + + services.openssh.enable = true; + services.xserver.displayManager.auto.user = "alice"; + + vuizvui.programs.gpg-agent.enable = true; + vuizvui.programs.gpg-agent.sshSupport = true; + programs.ssh.startAgent = false; + }; + + testScript = '' + $machine->waitForUnit("sshd.service"); + $machine->succeed("ssh-keygen -t ed25519 -f /root/id_ed25519 -N '''"); + my $cmd = 'mkdir -p ~/.ssh && cat > ~/.ssh/authorized_keys'; + $machine->succeed("su -c 'umask 0077; $cmd' alice < /root/id_ed25519.pub"); + + $machine->waitForX; + + sub ssh ($) { + my $esc = $_[0] =~ s/'/'\\${"'"}'/gr; + return "ssh -q -i /root/id_ed25519". + " -o StrictHostKeyChecking=no". + " alice\@127.0.0.1 -- '$esc'"; + } + + sub xsu ($) { + my $esc = $_[0] =~ s/'/'\\${"'"}'/gr; + return "DISPLAY=:0 su alice -c '$esc'"; + } + + $machine->nest("import snakeoil key", sub { + $machine->succeed(ssh "${cliTestWithPassphrase '' + gpg2 --import ${./snakeoil.asc} + ''}"); + $machine->succeed(ssh "${mkExpect '' + expect gpg> + send trust\r + expect decision? + send 5\r + expect "Do you really want" + send y\r + expect gpg> + send save\r + '' "gpg2 --edit-key ECC15FE1"}"); + }); + + subtest "test SSH agent support", sub { + $machine->succeed(ssh 'ssh-keygen -t ed25519 -f ~/testkey -N ""'); + $machine->succeed(ssh '${mkExpect '' + expect -regexp ---+.*Please.enter + send supersecret\r + expect -regexp ---+.*Please.re-en + send supersecret\r + '' "ssh-add ~/testkey"}'); + + $machine->succeed("umask 0077; $cmd < ~alice/testkey.pub"); + $machine->succeed(ssh 'rm ~/testkey*'); + + $machine->succeed(ssh 'ssh -o StrictHostKeyChecking=no root@127.0.0.1'. + ' touch /i_have_thu_powarr'); + $machine->succeed("test -e /i_have_thu_powarr"); + + $machine->succeed(ssh "systemctl --user reload gpg-agent"); + + $machine->succeed(ssh "${cliTestWithPassphrase '' + ssh -o StrictHostKeyChecking=no root@127.0.0.1 \ + touch /i_still_have_thu_powarr + ''}"); + $machine->succeed("test -e /i_still_have_thu_powarr"); + }; + + subtest "test from SSH", sub { + $machine->succeed(ssh "systemctl --user reload gpg-agent"); + $machine->succeed(ssh "${cliTestWithPassphrase '' + echo encrypt me > to_encrypt + gpg2 -sea -r ECC15FE1 to_encrypt + rm to_encrypt + ''}"); + $machine->succeed(ssh "${cliTest '' + [ "$(gpg2 -d to_encrypt.asc)" = "encrypt me" ] + ''}"); + }; + + subtest "test from X", sub { + $machine->succeed(ssh "systemctl --user reload gpg-agent"); + my $pid = $machine->succeed(xsu + 'echo encrypt me | gpg2 -sea -r ECC15FE1 > encrypted_x.asc & echo $!' + ); + chomp $pid; + $machine->waitForText(qr/Passphrase/); + $machine->screenshot("passphrase_dialog"); + $machine->sendChars("supersecret\n"); + $machine->waitUntilFails("kill -0 $pid"); + $machine->succeed(xsu '[ "$(gpg2 -d encrypted_x.asc)" = "encrypt me" ]'); + }; + ''; +} diff --git a/tests/programs/gpg-agent/snakeoil.asc b/tests/programs/gpg-agent/snakeoil.asc new file mode 100644 index 00000000..59c07011 --- /dev/null +++ b/tests/programs/gpg-agent/snakeoil.asc @@ -0,0 +1,59 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v2 + +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 +75FOlpj5VS82Sl0= +=3HD3 +-----END PGP PRIVATE KEY BLOCK----- |