From c52108234b62b388efbd44447dc2e572fbd16240 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Tue, 5 May 2015 20:16:23 +0100 Subject: [PATCH] Provide a Unix port of Pageant. This is much more like ssh-agent than the Windows version is - it sets SSH_AUTH_SOCK and SSH_AGENT_PID as its means of being found by other processes, rather than Windows Pageant's approach of establishing itself in a well-known location. But the actual agent code is the same as Windows Pageant. For the moment, this is an experimental utility and I don't expect it to be useful to many people; its immediate use to me is that it provides a way to test and debug the agent code on Unix, and also to use the agent interface as a convenient way to exercise public key functions I want to debug. And of course it means I can be constantly using and testing my own code, on whatever platform I happen to be using. In the further future, I have a list of possible features I might add to it, but I don't know which ones I'll decide are worthwhile. One feature I've already put in is a wider range of lifetime management options than ssh-agent: the -X mode causes Pageant to make a connection to your X display, and automatically terminate when that connection closes, so that it has the same lifetime as your X session without having to do the cumbersome trick of exec()ing the subsequent session-management process. --- Recipe | 4 + unix/uxpgnt.c | 517 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 521 insertions(+) create mode 100644 unix/uxpgnt.c diff --git a/Recipe b/Recipe index fa8a1a44..69d8431f 100644 --- a/Recipe +++ b/Recipe @@ -303,5 +303,9 @@ puttygen : [U] cmdgen sshrsag sshdssg sshprime sshdes sshbn sshmd5 version pscp : [U] pscp uxsftp uxcons UXSSH BE_SSH SFTP wildcard UXMISC psftp : [U] psftp uxsftp uxcons UXSSH BE_SSH SFTP wildcard UXMISC +pageant : [U] uxpgnt pageant sshrsa sshpubk sshdes sshbn sshmd5 version + + tree234 misc sshaes sshsha sshdss sshsh256 sshsh512 sshecc + + conf uxsignal nocproxy nogss be_none x11fwd ux_x11 UXMISC LIBS + PuTTY : [MX] osxmain OSXTERM OSXMISC CHARSET U_BE_ALL NONSSH UXSSH + ux_x11 uxpty uxsignal testback putty.icns info.plist diff --git a/unix/uxpgnt.c b/unix/uxpgnt.c new file mode 100644 index 00000000..7d1a6809 --- /dev/null +++ b/unix/uxpgnt.c @@ -0,0 +1,517 @@ +/* + * Unix Pageant, more or less similar to ssh-agent. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#define PUTTY_DO_GLOBALS /* actually _define_ globals */ +#include "putty.h" +#include "ssh.h" +#include "misc.h" +#include "pageant.h" + +SockAddr unix_sock_addr(const char *path); +Socket new_unix_listener(SockAddr listenaddr, Plug plug); + +void fatalbox(char *p, ...) +{ + va_list ap; + fprintf(stderr, "FATAL ERROR: "); + va_start(ap, p); + vfprintf(stderr, p, ap); + va_end(ap); + fputc('\n', stderr); + exit(1); +} +void modalfatalbox(char *p, ...) +{ + va_list ap; + fprintf(stderr, "FATAL ERROR: "); + va_start(ap, p); + vfprintf(stderr, p, ap); + va_end(ap); + fputc('\n', stderr); + exit(1); +} +void nonfatal(char *p, ...) +{ + va_list ap; + fprintf(stderr, "ERROR: "); + va_start(ap, p); + vfprintf(stderr, p, ap); + va_end(ap); + fputc('\n', stderr); +} +void connection_fatal(void *frontend, char *p, ...) +{ + va_list ap; + fprintf(stderr, "FATAL ERROR: "); + va_start(ap, p); + vfprintf(stderr, p, ap); + va_end(ap); + fputc('\n', stderr); + exit(1); +} +void cmdline_error(char *p, ...) +{ + va_list ap; + fprintf(stderr, "pageant: "); + va_start(ap, p); + vfprintf(stderr, p, ap); + va_end(ap); + fputc('\n', stderr); + exit(1); +} + +int pageant_logging = FALSE; +void pageant_log(void *ctx, const char *fmt, ...) +{ + va_list ap; + + if (!pageant_logging) + return; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + va_end(ap); +} + +/* + * In Pageant our selects are synchronous, so these functions are + * empty stubs. + */ +int uxsel_input_add(int fd, int rwx) { return 0; } +void uxsel_input_remove(int id) { } + +/* + * More stubs. + */ +void logevent(void *frontend, const char *string) {} +void random_save_seed(void) {} +void random_destroy_seed(void) {} +void noise_ultralight(unsigned long data) {} +char *platform_default_s(const char *name) { return NULL; } +int platform_default_i(const char *name, int def) { return def; } +FontSpec *platform_default_fontspec(const char *name) { return fontspec_new(""); } +Filename *platform_default_filename(const char *name) { return filename_from_str(""); } +char *x_get_default(const char *key) { return NULL; } +void old_keyfile_warning(void) {} +void timer_change_notify(unsigned long next) {} + +/* + * Short description of parameters. + */ +static void usage(void) +{ + printf("Pageant: SSH agent\n"); + printf("%s\n", ver); + printf("FIXME\n"); + exit(1); +} + +static void version(void) +{ + printf("pageant: %s\n", ver); + exit(1); +} + +void keylist_update(void) +{ + /* Nothing needs doing in Unix Pageant */ +} + +#define PAGEANT_DIR_PREFIX "/tmp/pageant" + +const char *const appname = "Pageant"; + +Conf *conf; + +char *platform_get_x_display(void) { + return dupstr(getenv("DISPLAY")); +} +int sshfwd_write(struct ssh_channel *c, char *data, int len) { return 0; } +void sshfwd_write_eof(struct ssh_channel *c) { /* FIXME: notify main loop instead */ exit(0); } +void sshfwd_unclean_close(struct ssh_channel *c, const char *err) { /* FIXME: notify main loop instead */ exit(1); } +void sshfwd_unthrottle(struct ssh_channel *c, int bufsize) {} +Conf *sshfwd_get_conf(struct ssh_channel *c) { return conf; } +void sshfwd_x11_sharing_handover(struct ssh_channel *c, + void *share_cs, void *share_chan, + const char *peer_addr, int peer_port, + int endian, int protomajor, int protominor, + const void *initial_data, int initial_len) {} +void sshfwd_x11_is_local(struct ssh_channel *c) {} +static void x11_log(Plug p, int type, SockAddr addr, int port, + const char *error_msg, int error_code) {} +static int x11_closing(Plug plug, const char *error_msg, int error_code, + int calling_back) { /* FIXME: notify main loop instead */ exit(0); } +static int x11_receive(Plug plug, int urgent, char *data, int len) { return 0; } +static void x11_sent(Plug plug, int bufsize) {} +struct X11Connection { + const struct plug_function_table *fn; +}; + +char *socketname; +void pageant_print_env(int pid) +{ + printf("SSH_AUTH_SOCK=%s; export SSH_AUTH_SOCK;\n" + "SSH_AGENT_PID=%d; export SSH_AGENT_PID;\n", + socketname, (int)pid); +} + +void pageant_fork_and_print_env(void) +{ + pid_t pid = fork(); + if (pid == -1) { + perror("fork"); + exit(1); + } else if (pid != 0) { + pageant_print_env(pid); + exit(0); + } + + /* + * Having forked off, we now daemonise ourselves as best we can. + * It's good practice in general to setsid() ourself out of any + * process group we didn't want to be part of, and to chdir("/") + * to avoid holding any directories open that we don't need in + * case someone wants to umount them; also, we should definitely + * close standard output (because it will very likely be pointing + * at a pipe from which some parent process is trying to read our + * environment variable dump, so if we hold open another copy of + * it then that process will never finish reading). We close + * standard input too on general principles, but not standard + * error, since we might need to shout a panicky error message + * down that one. + */ + if (chdir("/") < 0) { + /* should there be an error condition, nothing we can do about + * it anyway */ + } + close(0); + close(1); + setsid(); +} + +int signalpipe[2]; + +void sigchld(int signum) +{ + if (write(signalpipe[1], "x", 1) <= 0) + /* not much we can do about it */; +} + +int main(int argc, char **argv) +{ + int *fdlist; + int fd; + int i, fdcount, fdsize, fdstate; + int errors; + unsigned long now; + char *username, *socketdir; + const char *err; + struct pageant_listen_state *pl; + Socket sock; + enum { + LIFE_UNSPEC, LIFE_X11, LIFE_DEBUG, LIFE_PERM, LIFE_EXEC + } life = LIFE_UNSPEC; + const char *display = NULL; + int doing_opts = TRUE; + char **exec_args = NULL; + int termination_pid = -1; + + fdlist = NULL; + fdcount = fdsize = 0; + errors = FALSE; + + /* + * Process the command line. + */ + while (--argc) { + char *p = *++argv; + if (*p == '-' && doing_opts) { + if (!strcmp(p, "-V") || !strcmp(p, "--version")) { + version(); + } else if (!strcmp(p, "--help")) { + usage(); + exit(0); + } else if (!strcmp(p, "-X")) { + life = LIFE_X11; + } else if (!strcmp(p, "--debug")) { + life = LIFE_DEBUG; + } else if (!strcmp(p, "--permanent")) { + life = LIFE_PERM; + } else if (!strcmp(p, "--exec")) { + life = LIFE_EXEC; + } else if (!strcmp(p, "--")) { + doing_opts = FALSE; + } + } else { + if (life == LIFE_EXEC) { + exec_args = argv; + break; /* everything else is now args to the exec command */ + } else { + fprintf(stderr, "pageant: unexpected argument '%s'\n", p); + exit(1); + } + } + } + + if (errors) + return 1; + + if (life == LIFE_UNSPEC) { + fprintf(stderr, "pageant: expected a lifetime option\n"); + exit(1); + } + if (life == LIFE_EXEC && !exec_args) { + fprintf(stderr, "pageant: expected a command with --exec\n"); + exit(1); + } + + /* + * Block SIGPIPE, so that we'll get EPIPE individually on + * particular network connections that go wrong. + */ + putty_signal(SIGPIPE, SIG_IGN); + + sk_init(); + uxsel_init(); + + /* + * Set up a listening socket and run Pageant on it. + */ + username = get_username(); + socketdir = dupprintf("%s.%s", PAGEANT_DIR_PREFIX, username); + sfree(username); + assert(*socketdir == '/'); + if ((err = make_dir_and_check_ours(socketdir)) != NULL) { + fprintf(stderr, "pageant: %s: %s\n", socketdir, err); + exit(1); + } + socketname = dupprintf("%s/pageant.%d", socketdir, (int)getpid()); + + pageant_init(); + pl = pageant_listener_new(NULL, pageant_log); + sock = new_unix_listener(unix_sock_addr(socketname), (Plug)pl); + if ((err = sk_socket_error(sock)) != NULL) { + fprintf(stderr, "pageant: %s: %s\n", socketname, err); + exit(1); + } + pageant_listener_got_socket(pl, sock); + + conf = conf_new(); + conf_set_int(conf, CONF_proxy_type, PROXY_NONE); + + /* + * Lifetime preparations. + */ + signalpipe[0] = signalpipe[1] = -1; + if (life == LIFE_X11) { + struct X11Display *disp; + void *greeting; + int greetinglen; + Socket s; + struct X11Connection *conn; + + static const struct plug_function_table fn_table = { + x11_log, + x11_closing, + x11_receive, + x11_sent, + NULL + }; + + if (!display) + display = getenv("DISPLAY"); + if (!display) { + fprintf(stderr, "pageant: no DISPLAY for -X mode\n"); + exit(1); + } + disp = x11_setup_display(display, conf); + + conn = snew(struct X11Connection); + conn->fn = &fn_table; + s = new_connection(sk_addr_dup(disp->addr), + disp->realhost, disp->port, + 0, 1, 0, 0, (Plug)conn, conf); + if ((err = sk_socket_error(s)) != NULL) { + fprintf(stderr, "pageant: unable to connect to X server: %s", err); + exit(1); + } + greeting = x11_make_greeting('B', 11, 0, disp->localauthproto, + disp->localauthdata, + disp->localauthdatalen, + NULL, 0, &greetinglen); + sk_write(s, greeting, greetinglen); + smemclr(greeting, greetinglen); + sfree(greeting); + + pageant_fork_and_print_env(); + } else if (life == LIFE_PERM) { + pageant_fork_and_print_env(); + } else if (life == LIFE_DEBUG) { + pageant_print_env(getpid()); + pageant_logging = TRUE; + } else if (life == LIFE_EXEC) { + pid_t agentpid, pid; + + agentpid = getpid(); + + /* + * Set up the pipe we'll use to tell us about SIGCHLD. + */ + if (pipe(signalpipe) < 0) { + perror("pipe"); + exit(1); + } + putty_signal(SIGCHLD, sigchld); + + pid = fork(); + if (pid < 0) { + perror("fork"); + exit(1); + } else if (pid == 0) { + setenv("SSH_AUTH_SOCK", socketname, TRUE); + setenv("SSH_AGENT_PID", dupprintf("%d", (int)agentpid), TRUE); + execvp(exec_args[0], exec_args); + perror("exec"); + _exit(127); + } else { + termination_pid = pid; + } + } + /* + * FIXME: and if life == LIFE_EXEC, then we use our own pid, but + * don't print_env - instead, fork, stuff it in the real + * environment, and exec our child. + * + * FIXME: and hang on, perhaps LIFE_PARENT won't work after all, + * because after we fork, of course, our ppid _isn't_ an indicator + * of whether the target process has gone away. + */ + + now = GETTICKCOUNT(); + + while (1) { + fd_set rset, wset, xset; + int maxfd; + int rwx; + int ret; + unsigned long next; + + FD_ZERO(&rset); + FD_ZERO(&wset); + FD_ZERO(&xset); + maxfd = 0; + + if (signalpipe[0] >= 0) { + FD_SET_MAX(signalpipe[0], maxfd, rset); + } + + /* Count the currently active fds. */ + i = 0; + for (fd = first_fd(&fdstate, &rwx); fd >= 0; + fd = next_fd(&fdstate, &rwx)) i++; + + /* Expand the fdlist buffer if necessary. */ + if (i > fdsize) { + fdsize = i + 16; + fdlist = sresize(fdlist, fdsize, int); + } + + /* + * Add all currently open fds to the select sets, and store + * them in fdlist as well. + */ + fdcount = 0; + for (fd = first_fd(&fdstate, &rwx); fd >= 0; + fd = next_fd(&fdstate, &rwx)) { + fdlist[fdcount++] = fd; + if (rwx & 1) + FD_SET_MAX(fd, maxfd, rset); + if (rwx & 2) + FD_SET_MAX(fd, maxfd, wset); + if (rwx & 4) + FD_SET_MAX(fd, maxfd, xset); + } + + if (toplevel_callback_pending()) { + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + ret = select(maxfd, &rset, &wset, &xset, &tv); + } else if (run_timers(now, &next)) { + unsigned long then; + long ticks; + struct timeval tv; + + then = now; + now = GETTICKCOUNT(); + if (now - then > next - then) + ticks = 0; + else + ticks = next - now; + tv.tv_sec = ticks / 1000; + tv.tv_usec = ticks % 1000 * 1000; + ret = select(maxfd, &rset, &wset, &xset, &tv); + if (ret == 0) + now = next; + else + now = GETTICKCOUNT(); + } else { + ret = select(maxfd, &rset, &wset, &xset, NULL); + } + + if (ret < 0 && errno == EINTR) + continue; + + if (ret < 0) { + perror("select"); + exit(1); + } + + for (i = 0; i < fdcount; i++) { + fd = fdlist[i]; + /* + * We must process exceptional notifications before + * ordinary readability ones, or we may go straight + * past the urgent marker. + */ + if (FD_ISSET(fd, &xset)) + select_result(fd, 4); + if (FD_ISSET(fd, &rset)) + select_result(fd, 1); + if (FD_ISSET(fd, &wset)) + select_result(fd, 2); + } + + if (signalpipe[0] >= 0 && FD_ISSET(signalpipe[0], &rset)) { + char c[1]; + if (read(signalpipe[0], c, 1) <= 0) + /* ignore error */; + /* ignore its value; it'll be `x' */ + while (1) { + int status; + pid_t pid; + pid = waitpid(-1, &status, WNOHANG); + if (pid == 0) + break; + if (pid == termination_pid) + exit(0); + } + } + + run_toplevel_callbacks(); + } + + return 0; +} -- 2.45.2