├── LICENSE ├── Makefile ├── README.md ├── ssh-agent-card-prompt.1 └── ssh-agent-card-prompt.c /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 joshua stein 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # vim:ts=8 2 | 3 | PREFIX?= /usr/local 4 | X11BASE?= /usr/X11R6 5 | SYSCONFDIR?= /etc 6 | 7 | PKGLIBS= x11 xft 8 | 9 | CC?= cc 10 | CFLAGS+= -O2 -Wall \ 11 | -Wunused -Wmissing-prototypes -Wstrict-prototypes \ 12 | -Wpointer-sign \ 13 | `pkg-config --cflags ${PKGLIBS}` 14 | LDFLAGS+= `pkg-config --libs ${PKGLIBS}` 15 | 16 | # uncomment to enable debugging 17 | #CFLAGS+= -g 18 | 19 | BINDIR= $(PREFIX)/bin 20 | MANDIR= $(PREFIX)/man/man1 21 | 22 | SRC!= ls *.c 23 | OBJ= ${SRC:.c=.o} 24 | 25 | BIN= ssh-agent-card-prompt 26 | MAN= ssh-agent-card-prompt.1 27 | 28 | all: ${BIN} 29 | 30 | ssh-agent-card-prompt: $(OBJ) 31 | $(CC) -o $@ $(OBJ) $(LDFLAGS) 32 | 33 | install: all 34 | mkdir -p $(BINDIR) $(MANDIR) 35 | install -s $(BIN) $(BINDIR) 36 | install -m 644 $(MAN) $(MANDIR) 37 | 38 | clean: 39 | rm -f $(BIN) $(OBJ) 40 | 41 | .PHONY: all install clean 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssh-agent-card-prompt 2 | 3 | ![https://jcs.org/images/ssh-agent-card-prompt.png](https://jcs.org/images/ssh-agent-card-prompt.png) 4 | 5 | **ssh-agent-card-prompt** - 6 | prompt the user when SSH key signing requests to an 7 | ssh-agent(1) 8 | require tapping a physical security key (such as a YubiKey) 9 | 10 | # SYNOPSIS 11 | 12 | **ssh-agent-card-prompt** 13 | \[**-d**] 14 | \[**-p** *prompt*] 15 | 16 | # DESCRIPTION 17 | 18 | On startup, 19 | **ssh-agent-card-prompt** 20 | moves the current 21 | ssh-agent(1) 22 | socket (as set in the 23 | `SSH_AUTH_SOCK` 24 | environment variable) to a temporary location and creates a new socket at the 25 | location pointed to by that variable. 26 | 27 | When an SSH client connects, 28 | **ssh-agent-card-prompt** 29 | connects to the original 30 | ssh-agent(1) 31 | process and proxies requests and responses between the two. 32 | 33 | After 34 | **ssh-agent-card-prompt** 35 | detects and forwards an SSH\_AGENTC\_SIGN\_REQUEST message that appears to be for 36 | a PKCS key, 37 | ssh-agent(1) 38 | will block while waiting for the security key to be tapped and respond to the 39 | request. 40 | At that point, 41 | **ssh-agent-card-prompt** 42 | will present a modal X11 window with the 43 | *prompt* 44 | text and information about the process that is making the agent connection, 45 | reminding the user to tap the key. 46 | 47 | If the Escape key is pressed while presenting the dialog, the connections to 48 | the client and ssh-agent are immediately dropped. 49 | If the security key is tapped, 50 | ssh-agent(1) 51 | will send its response to 52 | **ssh-agent-card-prompt** 53 | which will then automatically close its X11 window. 54 | 55 | When 56 | **ssh-agent-card-prompt** 57 | exits, the original ssh-agent socket is moved back to the path pointed to by 58 | the 59 | `SSH_AUTH_SOCK` 60 | variable. 61 | 62 | # CONFIGURATION 63 | 64 | Your security key should be configured to require touch confirmation for this 65 | to be of any use. 66 | 67 | For YubiKey keys, this can be done with 68 | yubico-piv-tool(1) 69 | by supplying 70 | **--touch-policy=always** 71 | when creating/importing keys. 72 | 73 | # OPTIONS 74 | 75 | **-d** 76 | 77 | > Print debugging messages to the terminal. 78 | > If specified twice, the contents of each message passed will be printed to the 79 | > terminal. 80 | 81 | **-p** *prompt* 82 | 83 | > The text presented to the user in the modal dialog. 84 | > Defaults to "Tap the security key to continue with signing request". 85 | 86 | # SEE ALSO 87 | 88 | ssh-agent(1) 89 | 90 | # AUTHORS 91 | 92 | **ssh-agent-card-prompt** 93 | was written by 94 | joshua stein <[jcs@jcs.org](mailto:jcs@jcs.org)>. 95 | 96 | OpenBSD 6.6 - October 15, 2019 97 | -------------------------------------------------------------------------------- /ssh-agent-card-prompt.1: -------------------------------------------------------------------------------- 1 | .Dd $Mdocdate: October 15 2019$ 2 | .Dt SSH-AGENT-CARD-PROMPT 1 3 | .Os 4 | .Sh NAME 5 | .Nm ssh-agent-card-prompt 6 | .Nd 7 | prompt the user when SSH key signing requests to an 8 | .Xr ssh-agent 1 9 | require tapping a physical security key (such as a YubiKey) 10 | .Sh SYNOPSIS 11 | .Nm 12 | .Op Fl d 13 | .Op Fl p Ar prompt 14 | .Sh DESCRIPTION 15 | On startup, 16 | .Nm 17 | moves the current 18 | .Xr ssh-agent 1 19 | socket (as set in the 20 | .Ev SSH_AUTH_SOCK 21 | environment variable) to a temporary location and creates a new socket at the 22 | location pointed to by that variable. 23 | .Pp 24 | When an SSH client connects, 25 | .Nm 26 | connects to the original 27 | .Xr ssh-agent 1 28 | process and proxies requests and responses between the two. 29 | .Pp 30 | After 31 | .Nm 32 | detects and forwards an SSH_AGENTC_SIGN_REQUEST message that appears to be for 33 | a PKCS key, 34 | .Xr ssh-agent 1 35 | will block while waiting for the security key to be tapped and respond to the 36 | request. 37 | At that point, 38 | .Nm 39 | will present a modal X11 window with the 40 | .Ar prompt 41 | text and information about the process that is making the agent connection, 42 | reminding the user to tap the key. 43 | .Pp 44 | If the Escape key is pressed while presenting the dialog, the connections to 45 | the client and ssh-agent are immediately dropped. 46 | If the security key is tapped, 47 | .Xr ssh-agent 1 48 | will send its response to 49 | .Nm 50 | which will then automatically close its X11 window. 51 | .Pp 52 | When 53 | .Nm 54 | exits, the original ssh-agent socket is moved back to the path pointed to by 55 | the 56 | .Ev SSH_AUTH_SOCK 57 | variable. 58 | .Sh CONFIGURATION 59 | Your security key should be configured to require touch confirmation for this 60 | to be of any use. 61 | .Pp 62 | For YubiKey keys, this can be done with 63 | .Xr yubico-piv-tool 1 64 | by supplying 65 | .Cm --touch-policy=always 66 | when creating/importing keys. 67 | .Sh OPTIONS 68 | .Bl -tag -width Ds 69 | .It Fl d 70 | Print debugging messages to the terminal. 71 | If specified twice, the contents of each message passed will be printed to the 72 | terminal. 73 | .It Fl p Ar prompt 74 | The text presented to the user in the modal dialog. 75 | Defaults to "Tap the security key to continue with signing request". 76 | .Sh SEE ALSO 77 | .Xr ssh-agent 1 78 | .Sh AUTHORS 79 | .Nm 80 | was written by 81 | .An joshua stein Aq Mt jcs@jcs.org . 82 | -------------------------------------------------------------------------------- /ssh-agent-card-prompt.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 joshua stein 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | #define FONT "monospace:size=24" 37 | #define FONT_SMALL "monospace:size=18" 38 | #define DEFAULT_PROMPT "Tap the security key to continue with signing request" 39 | 40 | /* https://tools.ietf.org/html/draft-miller-ssh-agent-03 */ 41 | struct agent_msg_hdr { 42 | uint32_t len; 43 | uint8_t type; 44 | #define SSH_AGENT_IDENTITIES_ANSWER 12 45 | #define SSH_AGENTC_SIGN_REQUEST 13 46 | } __packed; 47 | 48 | uint32_t be32bytes(unsigned char *); 49 | void rollback(int); 50 | int forward_agent_message(ssize_t, void *, int, pid_t); 51 | void x11_init(void); 52 | int x11_prompt(pid_t); 53 | int procname(pid_t, char **); 54 | 55 | static char *auth_sock = NULL, *upstream_auth_sock = NULL; 56 | static char pkcs_key[1024]; 57 | static unsigned int pkcs_key_len = 0; 58 | static Display *dpy = NULL; 59 | static XVisualInfo vinfo; 60 | static Colormap colormap; 61 | static XftFont *font, *smallfont; 62 | static XftColor white, gray; 63 | static int upstreamfd = -1; 64 | static int debug = 0; 65 | static char *prompt; 66 | 67 | #define DPRINTF(x) { if (debug) { printf x; } }; 68 | 69 | #ifndef explicit_bzero 70 | #define explicit_bzero(p, s) memset(p, 0, s) 71 | #endif 72 | 73 | uint32_t 74 | be32bytes(unsigned char *buf) 75 | { 76 | return (buf[3] << 0) | (buf[2] << 8) | (buf[1] << 16) | (buf[0] << 24); 77 | } 78 | 79 | void 80 | rollback(int sig) 81 | { 82 | if (upstream_auth_sock != NULL && auth_sock != NULL) { 83 | DPRINTF(("rollback: %s -> %s\n", upstream_auth_sock, 84 | auth_sock)); 85 | if (rename(upstream_auth_sock, auth_sock) == -1) 86 | warn("rollback rename failed"); 87 | } 88 | 89 | if (sig) 90 | _exit(0); 91 | } 92 | 93 | int 94 | main(int argc, char *argv[]) 95 | { 96 | struct sockaddr_un sunaddr; 97 | struct sockpeercred peercred; 98 | struct pollfd pfd[2]; 99 | ssize_t len; 100 | socklen_t slen; 101 | unsigned char buf[4096]; 102 | uid_t euid; 103 | gid_t egid; 104 | pid_t pid; 105 | int ch, sock, clientfd = -1; 106 | 107 | prompt = strdup(DEFAULT_PROMPT); 108 | 109 | while ((ch = getopt(argc, argv, "dp:")) != -1) { 110 | switch (ch) { 111 | case 'd': 112 | debug++; 113 | break; 114 | case 'p': 115 | free(prompt); 116 | prompt = strdup(optarg); 117 | if (prompt == NULL) 118 | err(1, "strdup"); 119 | break; 120 | default: 121 | exit(1); 122 | } 123 | } 124 | argc -= optind; 125 | argv += optind; 126 | 127 | if ((auth_sock = getenv("SSH_AUTH_SOCK")) == NULL) 128 | errx(1, "no SSH_AUTH_SOCK set"); 129 | 130 | len = strlen(auth_sock) + 6; 131 | if ((upstream_auth_sock = malloc(len)) == NULL) 132 | err(1, "malloc"); 133 | 134 | /* move aside and let the man go through */ 135 | snprintf(upstream_auth_sock, len, "%s.orig", auth_sock); 136 | DPRINTF(("%s -> %s\n", auth_sock, upstream_auth_sock)); 137 | rename(auth_sock, upstream_auth_sock); 138 | 139 | /* listen on SSH_AUTH_SOCK path */ 140 | memset(&sunaddr, 0, sizeof(sunaddr)); 141 | sunaddr.sun_family = AF_UNIX; 142 | if (strlcpy(sunaddr.sun_path, auth_sock, 143 | sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) { 144 | rollback(0); 145 | err(1, "strlcpy"); 146 | } 147 | 148 | sock = socket(PF_UNIX, SOCK_STREAM, 0); 149 | if (sock == -1) { 150 | rollback(0); 151 | err(1, "socket"); 152 | } 153 | 154 | if (bind(sock, (struct sockaddr *)&sunaddr, sizeof(sunaddr)) == -1) { 155 | rollback(0); 156 | err(1, "bind"); 157 | } 158 | 159 | if (listen(sock, 128) == -1) { 160 | rollback(0); 161 | err(1, "listen"); 162 | } 163 | 164 | #ifdef __OpenBSD__ 165 | if (pledge("stdio unix rpath cpath ps proc", NULL) == -1) 166 | err(1, "pledge"); 167 | #endif 168 | 169 | signal(SIGCHLD, SIG_IGN); 170 | signal(SIGPIPE, SIG_IGN); 171 | signal(SIGINT, rollback); 172 | signal(SIGTERM, rollback); 173 | 174 | /* forward connections to SSH_AUTH_SOCK to SSH_AUTH_SOCK+.orig */ 175 | for (;;) { 176 | clientfd = accept(sock, (struct sockaddr *)&sunaddr, &slen); 177 | if (clientfd == -1) { 178 | warn("accept"); 179 | continue; 180 | } 181 | 182 | pid = fork(); 183 | if (pid == -1) 184 | err(1, "fork"); 185 | else if (pid != 0) { 186 | /* parent */ 187 | close(clientfd); 188 | continue; 189 | } 190 | 191 | if (getpeereid(clientfd, &euid, &egid) == -1) { 192 | warn("[%d] getpeereid", getpid()); 193 | goto close; 194 | } 195 | 196 | if (euid != 0 && getuid() != euid) { 197 | warn("[%d] socket peer uid %u != uid %u", getpid(), 198 | (u_int)euid, (u_int)getuid()); 199 | goto close; 200 | } 201 | 202 | if (getsockopt(clientfd, SOL_SOCKET, SO_PEERCRED, &peercred, 203 | &slen) == -1) { 204 | warn("[%d] getsockopt(SO_PEERCRED)", getpid()); 205 | goto close; 206 | } 207 | 208 | DPRINTF(("[%d] got client connection from pid %d\n", getpid(), 209 | peercred.pid)); 210 | 211 | if ((upstreamfd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { 212 | warn("[%d] socket", getpid()); 213 | goto close; 214 | } 215 | 216 | memset(&sunaddr, 0, sizeof(sunaddr)); 217 | sunaddr.sun_family = AF_UNIX; 218 | if (strlcpy(sunaddr.sun_path, upstream_auth_sock, 219 | sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) { 220 | warn("[%d] strlcpy", getpid()); 221 | goto close; 222 | } 223 | 224 | if (connect(upstreamfd, (struct sockaddr *)&sunaddr, 225 | sizeof(sunaddr)) == -1) { 226 | warn("[%d] connect to upstream", getpid()); 227 | goto close; 228 | } 229 | 230 | DPRINTF(("[%d] got upstream connection\n", getpid())); 231 | 232 | /* do this early so we can pledge */ 233 | x11_init(); 234 | 235 | if (pledge("stdio ps", NULL) == -1) 236 | err(1, "[%d] pledge", getpid()); 237 | 238 | pkcs_key_len = 0; 239 | 240 | memset(&pfd, 0, sizeof(pfd)); 241 | pfd[0].fd = clientfd; 242 | pfd[0].events = POLLIN; 243 | pfd[1].fd = upstreamfd; 244 | pfd[1].events = POLLIN; 245 | 246 | for (;;) { 247 | if (poll(pfd, 2, INFTIM) == -1) { 248 | warn("[%d] poll failed", getpid()); 249 | goto close; 250 | } 251 | 252 | if ((pfd[0].revents & (POLLIN|POLLHUP))) { 253 | /* client -> upstream */ 254 | len = read(clientfd, buf, sizeof(buf)); 255 | DPRINTF(("[%d] got client data (%zu)\n", 256 | getpid(), len)); 257 | if (len && (forward_agent_message(len, &buf, 258 | upstreamfd, peercred.pid) == -1)) 259 | goto close; 260 | if (pfd[0].revents & POLLHUP) 261 | goto close; 262 | } 263 | 264 | if ((pfd[1].revents & (POLLIN|POLLHUP))) { 265 | /* upstream -> client */ 266 | len = read(upstreamfd, buf, sizeof(buf)); 267 | DPRINTF(("[%d] got upstream data (%zu)\n", 268 | getpid(), len)); 269 | if (len && (forward_agent_message(len, &buf, 270 | clientfd, peercred.pid) == -1)) 271 | goto close; 272 | if (pfd[1].revents & POLLHUP) 273 | goto close; 274 | } 275 | } 276 | 277 | close: 278 | explicit_bzero(&buf, sizeof(buf)); 279 | explicit_bzero(&pkcs_key, sizeof(pkcs_key)); 280 | 281 | DPRINTF(("[%d] closing client and upstream connections\n", 282 | getpid())); 283 | 284 | if (clientfd != -1) { 285 | close(clientfd); 286 | clientfd = -1; 287 | } 288 | if (upstreamfd != -1) { 289 | close(upstreamfd); 290 | upstreamfd = -1; 291 | } 292 | 293 | if (dpy) 294 | XCloseDisplay(dpy); 295 | 296 | exit(0); 297 | } 298 | 299 | /* unreached */ 300 | return 0; 301 | } 302 | 303 | int 304 | forward_agent_message(ssize_t len, void *buf, int destfd, pid_t clientpid) 305 | { 306 | struct agent_msg_hdr *hdr = (struct agent_msg_hdr *)buf; 307 | uint32_t nkeys, klen; 308 | ssize_t off; 309 | int x; 310 | 311 | if (debug >= 2) { 312 | DPRINTF(("[%d] forwarding data[%zu]:", getpid(), len)); 313 | for (x = 0; x < len; x++) 314 | DPRINTF((" %02x", ((unsigned char *)buf)[x])); 315 | DPRINTF(("\n")); 316 | } 317 | 318 | if (len < sizeof(struct agent_msg_hdr)) { 319 | warnx("[%d] short message (%zu < %zu)", getpid(), len, 320 | sizeof(struct agent_msg_hdr)); 321 | return -1; 322 | } 323 | 324 | if (len != (be32toh(hdr->len) + sizeof(uint32_t))) { 325 | warn("[%d] message invalid len (%zu != %d + %zu)", getpid(), 326 | len, be32toh(hdr->len), sizeof(uint32_t)); 327 | return -1; 328 | } 329 | 330 | if (write(destfd, buf, len) != len) { 331 | warn("[%d] write to destfd failed", getpid()); 332 | return -1; 333 | } 334 | 335 | off = sizeof(struct agent_msg_hdr); 336 | len -= off; 337 | buf += off; 338 | 339 | /* 340 | * The normal process for an SSH connection is for SSH to make an 341 | * SSH_AGENTC_REQUEST_IDENTITIES request to the agent, the agent 342 | * replies with SSH_AGENT_IDENTITIES_ANSWER with all of the key 343 | * information, and then SSH makes a SSH_AGENTC_SIGN_REQUEST request 344 | * with that key. 345 | * 346 | * Watch for an SSH_AGENT_IDENTITIES_ANSWER message and parse out the 347 | * keys, making note of which key has a comment that looks like a pkcs 348 | * key, so when we see that key blob in SSH_AGENTC_SIGN_REQUEST, we 349 | * know it's for our key that needs a touch confirmation. 350 | */ 351 | switch (hdr->type) { 352 | case SSH_AGENT_IDENTITIES_ANSWER: 353 | if (len < sizeof(uint32_t)) { 354 | DPRINTF(("[%d] SSH_AGENT_IDENTITIES_ANSWER but " 355 | "remaining len too short\n", getpid())); 356 | break; 357 | } 358 | 359 | nkeys = be32bytes(buf); 360 | len -= sizeof(uint32_t); 361 | buf += sizeof(uint32_t); 362 | if (nkeys <= 0) 363 | break; 364 | 365 | DPRINTF(("[%d] SSH_AGENT_IDENTITIES_ANSWER with %d key(s)\n", 366 | getpid(), nkeys)); 367 | 368 | for (x = 0; x < nkeys && len > 0; x++) { 369 | uint32_t kbloblen, kcommlen; 370 | char *kblob, *kcomm; 371 | 372 | /* key blob len */ 373 | if (len < sizeof(uint32_t)) { 374 | warn("[%d] SSH_AGENT_IDENTITIES_ANSWER short " 375 | "(1)", getpid()); 376 | break; 377 | } 378 | kbloblen = be32bytes(buf); 379 | len -= sizeof(uint32_t); 380 | buf += sizeof(uint32_t); 381 | 382 | if (kbloblen > len) { 383 | warn("[%d] SSH_AGENT_IDENTITIES_ANSWER short " 384 | "(2)", getpid()); 385 | break; 386 | } 387 | if (kbloblen <= 0) 388 | continue; 389 | 390 | /* key blob */ 391 | len -= kbloblen; 392 | kblob = buf; 393 | buf += kbloblen; 394 | 395 | /* key comment len */ 396 | if (len < sizeof(uint32_t)) { 397 | warn("[%d] SSH_AGENT_IDENTITIES_ANSWER short " 398 | "(3)", getpid()); 399 | break; 400 | } 401 | kcommlen = be32bytes(buf); 402 | len -= sizeof(uint32_t); 403 | buf += sizeof(uint32_t); 404 | 405 | if (kcommlen > len) { 406 | warn("[%d] SSH_AGENT_IDENTITIES_ANSWER short " 407 | "(4)", getpid()); 408 | break; 409 | } 410 | if (kcommlen <= 0) 411 | continue; 412 | 413 | /* key comment */ 414 | kcomm = malloc(kcommlen + 2); 415 | if (kcomm == NULL) { 416 | warn("[%d] malloc %d", getpid(), kcommlen + 2); 417 | break; 418 | } 419 | strlcpy(kcomm, buf, kcommlen + 1); 420 | DPRINTF(("[%d] key[%d] = %s\n", getpid(), x, kcomm)); 421 | 422 | /* match on pkcs11 or pkcs15 */ 423 | if (strstr(kcomm, "pkcs1") != NULL || 424 | strstr(kcomm, "PIV AUTH pubkey") != NULL) { 425 | DPRINTF(("[%d] found pkcs1 key at %d\n", 426 | getpid(), x)); 427 | pkcs_key_len = kbloblen; 428 | memcpy(pkcs_key, kblob, kbloblen); 429 | } 430 | free(kcomm); 431 | 432 | len -= kcommlen; 433 | buf += kcommlen; 434 | } 435 | break; 436 | 437 | case SSH_AGENTC_SIGN_REQUEST: 438 | /* key blob len */ 439 | if (len < sizeof(uint32_t)) { 440 | DPRINTF(("[%d] SSH_AGENTC_SIGN_REQUEST but remaining " 441 | "len too short\n", getpid())); 442 | break; 443 | } 444 | klen = be32bytes(buf); 445 | len -= sizeof(uint32_t); 446 | buf += sizeof(uint32_t); 447 | 448 | if (klen > len) { 449 | warn("[%d] SSH_AGENTC_SIGN_REQUEST short (1)", 450 | getpid()); 451 | break; 452 | } 453 | if (klen <= 0) 454 | break; 455 | 456 | if (klen > 0 && klen == pkcs_key_len && 457 | memcmp(buf, pkcs_key, pkcs_key_len) == 0) { 458 | DPRINTF(("[%d] SSH_AGENTC_SIGN_REQUEST for our pkcs " 459 | "key\n", getpid())); 460 | if (x11_prompt(clientpid) == -1) 461 | return -1; 462 | } 463 | } 464 | 465 | return 0; 466 | } 467 | 468 | void 469 | x11_init(void) 470 | { 471 | dpy = XOpenDisplay(NULL); 472 | if (!dpy) 473 | errx(1, "[%d] XOpenDisplay failed", getpid()); 474 | 475 | if (!XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo)) 476 | errx(1, "[%d] !XMatchVisualInfo failed", getpid()); 477 | 478 | colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, 479 | AllocNone); 480 | 481 | font = XftFontOpenName(dpy, DefaultScreen(dpy), FONT); 482 | if (font == NULL) 483 | errx(1, "[%d] failed opening font", getpid()); 484 | 485 | smallfont = XftFontOpenName(dpy, DefaultScreen(dpy), FONT_SMALL); 486 | if (smallfont == NULL) 487 | errx(1, "[%d] failed opening small font", getpid()); 488 | 489 | if (!XftColorAllocName(dpy, vinfo.visual, colormap, "white", &white)) 490 | errx(1, "[%d] failed allocating white", getpid()); 491 | if (!XftColorAllocName(dpy, vinfo.visual, colormap, "#eeeeee", &gray)) 492 | errx(1, "[%d] failed allocating gray", getpid()); 493 | } 494 | 495 | int 496 | x11_prompt(pid_t clientpid) 497 | { 498 | XSetWindowAttributes attr; 499 | Window win; 500 | XEvent ev; 501 | XftDraw *draw; 502 | XGlyphInfo gi; 503 | struct pollfd pfd[2]; 504 | size_t len; 505 | char *clientproc, *word, *line; 506 | int grab, x, y, ret = -1; 507 | 508 | attr.colormap = colormap; 509 | attr.override_redirect = 1; 510 | attr.border_pixel = 0; 511 | attr.background_pixel = 0x28000000; /* 40% opacity */ 512 | 513 | win = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, 514 | DisplayWidth(dpy, DefaultScreen(dpy)), 515 | DisplayHeight(dpy, DefaultScreen(dpy)), 0, 516 | vinfo.depth, InputOutput, vinfo.visual, 517 | CWOverrideRedirect|CWColormap|CWBorderPixel|CWBackPixel, &attr); 518 | 519 | draw = XftDrawCreate(dpy, win, vinfo.visual, colormap); 520 | if (!draw) { 521 | warnx("[%d] can't draw with font", getpid()); 522 | ret = -1; 523 | goto done_x; 524 | } 525 | 526 | /* if we can't grab the keyboard, maybe another child has it */ 527 | for (x = 0; x < 30; x++) { 528 | grab = XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, 529 | GrabModeAsync, GrabModeAsync, CurrentTime); 530 | if (grab == GrabSuccess) 531 | break; 532 | 533 | warn("[%d] couldn't grab keyboard", getpid()); 534 | sleep(1); 535 | } 536 | if (grab != GrabSuccess) { 537 | warn("[%d] couldn't grab keyboard, giving up", getpid()); 538 | ret = -1; 539 | goto done_x; 540 | } 541 | 542 | XMapRaised(dpy, win); 543 | 544 | /* draw prompt */ 545 | XftTextExtentsUtf8(dpy, font, (FcChar8 *)prompt, strlen(prompt), &gi); 546 | y = (DisplayHeight(dpy, DefaultScreen(dpy)) / 2) - (gi.height * 1.2); 547 | XftDrawStringUtf8(draw, &white, font, 548 | (DisplayWidth(dpy, DefaultScreen(dpy)) / 2) - (gi.width / 2), y, 549 | (FcChar8 *)prompt, strlen(prompt)); 550 | y += (gi.height * 1.3); 551 | 552 | /* then add process info */ 553 | if (procname(clientpid, &clientproc) == -1) { 554 | clientproc = strdup("(failed finding process info)"); 555 | if (clientproc == NULL) 556 | err(1, "[%d] malloc", getpid()); 557 | } 558 | line = malloc(strlen(clientproc) + 20); 559 | if (line == NULL) 560 | err(1, "[%d] malloc", getpid()); 561 | snprintf(line, strlen(clientproc) + 20, "PID %d: %s", clientpid, 562 | clientproc); 563 | clientproc = line; 564 | 565 | /* process info may be long, so wrap it to multiple lines */ 566 | line = strdup(""); 567 | if (line == NULL) 568 | err(1, "[%d] strdup", getpid()); 569 | for ((word = strsep(&clientproc, " ")); word && *word != '\0'; 570 | (word = strsep(&clientproc, " "))) { 571 | char *oldline = strdup(line); 572 | if (oldline == NULL) 573 | err(1, "[%d] strdup", getpid()); 574 | 575 | len = strlen(line) + 1 + strlen(word) + 1; 576 | line = realloc(line, len); 577 | 578 | if (line[0] != '\0') 579 | strlcat(line, " ", len); 580 | 581 | strlcat(line, word, len); 582 | 583 | XftTextExtentsUtf8(dpy, smallfont, (FcChar8 *)line, 584 | strlen(line), &gi); 585 | if (gi.width > (DisplayWidth(dpy, DefaultScreen(dpy)) * 0.9)) { 586 | /* this line is now too long, draw the old one */ 587 | XftTextExtentsUtf8(dpy, smallfont, (FcChar8 *)oldline, 588 | strlen(oldline), &gi); 589 | XftDrawStringUtf8(draw, &gray, smallfont, 590 | (DisplayWidth(dpy, DefaultScreen(dpy)) / 2) - 591 | (gi.width / 2), y, (FcChar8 *)oldline, 592 | strlen(oldline)); 593 | y += (gi.height * 1.2); 594 | 595 | free(line); 596 | line = strdup(word); 597 | if (line == NULL) 598 | err(1, "strdup"); 599 | } 600 | } 601 | 602 | if (line != NULL && line[0] != '\0') { 603 | XftTextExtentsUtf8(dpy, smallfont, (FcChar8 *)line, 604 | strlen(line), &gi); 605 | XftDrawStringUtf8(draw, &gray, smallfont, 606 | (DisplayWidth(dpy, DefaultScreen(dpy)) / 2) - 607 | (gi.width / 2), y, (FcChar8 *)line, strlen(line)); 608 | free(line); 609 | } 610 | free(clientproc); 611 | 612 | XSelectInput(dpy, win, StructureNotifyMask); 613 | XSync(dpy, False); 614 | 615 | memset(&pfd, 0, sizeof(pfd)); 616 | pfd[0].fd = ConnectionNumber(dpy); 617 | pfd[0].events = POLLIN; 618 | pfd[1].fd = upstreamfd; 619 | pfd[1].events = POLLIN; 620 | 621 | for (;;) { 622 | if (poll(pfd, 2, INFTIM) < 1) 623 | continue; 624 | 625 | if ((pfd[0].revents & (POLLIN|POLLHUP))) { 626 | XNextEvent(dpy, &ev); 627 | DPRINTF(("[%d] got X11 event of type %d\n", ev.type, 628 | getpid())); 629 | if (ev.type == KeyPress) { 630 | if (XLookupKeysym(&ev.xkey, 0) == XK_Escape) { 631 | DPRINTF(("[%d] escape pressed\n", 632 | getpid())); 633 | ret = -1; 634 | break; 635 | } else { 636 | DPRINTF(("key pressed, not escape\n")); 637 | } 638 | } else { 639 | DPRINTF(("[%d] got other X11 event, " 640 | "re-raising\n", getpid())); 641 | XRaiseWindow(dpy, win); 642 | } 643 | } 644 | 645 | if ((pfd[1].revents & (POLLIN|POLLHUP))) { 646 | /* 647 | * gpg-agent is sending back data from 648 | * SSH_AGENTC_SIGN_REQUEST, the key was touched 649 | */ 650 | DPRINTF(("[%d] got data from upstream, key must have " 651 | "been touched\n", getpid())); 652 | ret = 0; 653 | break; 654 | } 655 | } 656 | 657 | done_x: 658 | XftDrawDestroy(draw); 659 | XUngrabKeyboard(dpy, CurrentTime); 660 | XDestroyWindow(dpy, win); 661 | XSync(dpy, False); 662 | 663 | return ret; 664 | } 665 | 666 | int 667 | procname(pid_t pid, char **outbuf) 668 | { 669 | #ifdef __OpenBSD__ 670 | char *buf = NULL, **args; 671 | size_t buflen = 128; 672 | int mib[6] = { CTL_KERN, KERN_PROC_ARGS, 0, KERN_PROC_ARGV, 0, 0 }; 673 | 674 | buf = malloc(buflen); 675 | if (buf == NULL) 676 | err(1, "[%d] malloc", getpid()); 677 | 678 | /* fetch process args */ 679 | mib[0] = CTL_KERN; 680 | mib[1] = KERN_PROC_ARGS; 681 | mib[2] = pid; 682 | mib[3] = KERN_PROC_ARGV; 683 | 684 | /* keep increasing buf size until it fits */ 685 | while (sysctl(mib, 4, buf, &buflen, NULL, 0) == -1) { 686 | if (errno != ENOMEM) { 687 | free(buf); 688 | return -1; 689 | } 690 | 691 | if ((buf = realloc(buf, buflen + 128)) == NULL) 692 | err(1, "[%d] realloc", getpid()); 693 | buflen += 128; 694 | } 695 | 696 | args = (char **)buf; 697 | if (args[0] == NULL) { 698 | free(buf); 699 | return -1; 700 | } 701 | 702 | *outbuf = malloc(1); 703 | if (*outbuf == NULL) 704 | err(1, "[%d] malloc", getpid()); 705 | 706 | *outbuf[0] = '\0'; 707 | buflen = 1; 708 | while (*args != NULL) { 709 | if (*outbuf[0] != '\0') 710 | buflen += 1; 711 | buflen += strlen(*args) + 1; 712 | *outbuf = realloc(*outbuf, buflen); 713 | if (*outbuf == NULL) 714 | err(1, "[%d] realloc", getpid()); 715 | if (*outbuf[0] != '\0') 716 | strlcat(*outbuf, " ", buflen); 717 | strlcat(*outbuf, *args, buflen); 718 | args++; 719 | } 720 | 721 | DPRINTF(("[%d] PID %d: %s\n", getpid(), pid, *outbuf)); 722 | 723 | free(buf); 724 | return 0; 725 | #else 726 | warn("[%d] procname not implemented", getpid()); 727 | return -1; 728 | #endif 729 | } 730 | --------------------------------------------------------------------------------