Call reset_async() after using PWMD_OPTION_PASSPHRASE or
[libpwmd.git] / src / ssh.c
blob27d1da358372504f6f7bcaae672d0f7ab2b0b147
1 /* vim:tw=78:ts=8:sw=4:set ft=c: */
2 /*
3 Copyright (C) 2006-2009 Ben Kibbey <bjk@luxsci.net>
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02110-1301 USA
19 #include <stdlib.h>
20 #include <unistd.h>
21 #include <fcntl.h>
22 #include <errno.h>
23 #include <err.h>
24 #include <pwd.h>
25 #include <netdb.h>
26 #include <netinet/in.h>
27 #include <sys/socket.h>
28 #include <ctype.h>
30 #ifdef HAVE_CONFIG_H
31 #include <config.h>
32 #endif
34 #ifdef WITH_LIBPTH
35 #include <pth.h>
36 #endif
38 #include "misc.h"
39 #include "ssh.h"
41 static void ssh_deinit(pwmd_tcp_conn_t *conn)
43 if (!conn)
44 return;
46 if (conn->channel) {
47 libssh2_channel_close(conn->channel);
48 libssh2_channel_free(conn->channel);
51 if (conn->session) {
52 libssh2_session_disconnect(conn->session, N_("libpwmd saying bye!"));
53 libssh2_session_free(conn->session);
56 conn->session = NULL;
57 conn->channel = NULL;
58 _free_ssh_conn(conn);
61 static int read_hook(assuan_context_t ctx, assuan_fd_t fd, void *data,
62 size_t len, ssize_t *ret)
64 pwm_t *pwm = assuan_get_pointer(ctx);
66 if (!pwm || !pwm->tcp_conn)
67 #ifdef WITH_LIBPTH
68 *ret = pth_recv((int)fd, data, len, 0);
69 #else
70 *ret = recv((int)fd, data, len, 0);
71 #endif
72 else
73 *ret = libssh2_channel_read(pwm->tcp_conn->channel, data, len);
75 return *ret >= 0 ? 1 : 0;
78 static int write_hook(assuan_context_t ctx, assuan_fd_t fd, const void *data,
79 size_t len, ssize_t *ret)
81 pwm_t *pwm = assuan_get_pointer(ctx);
83 if (!pwm || !pwm->tcp_conn)
84 #ifdef WITH_LIBPTH
85 *ret = pth_send((int)fd, data, len, 0);
86 #else
87 *ret = send((int)fd, data, len, 0);
88 #endif
89 else
90 *ret = libssh2_channel_write(pwm->tcp_conn->channel, data, len);
92 return *ret >= 0 ? 1 : 0;
95 void _free_ssh_conn(pwmd_tcp_conn_t *conn)
97 if (!conn)
98 return;
100 if (conn->username) {
101 pwmd_free(conn->username);
102 conn->username = NULL;
105 if (conn->known_hosts) {
106 pwmd_free(conn->known_hosts);
107 conn->known_hosts = NULL;
110 if (conn->identity) {
111 pwmd_free(conn->identity);
112 conn->identity = NULL;
115 if (conn->identity_pub) {
116 pwmd_free(conn->identity_pub);
117 conn->identity_pub = NULL;
120 if (conn->host) {
121 pwmd_free(conn->host);
122 conn->host = NULL;
125 if (conn->hostkey) {
126 pwmd_free(conn->hostkey);
127 conn->hostkey = NULL;
130 if (conn->chan) {
131 ares_destroy(conn->chan);
132 conn->chan = NULL;
135 if (conn->he) {
136 ares_free_hostent(conn->he);
137 conn->he = NULL;
140 if (!conn->session && conn->fd >= 0) {
141 close(conn->fd);
142 conn->fd = -1;
145 if (conn->session)
146 ssh_deinit(conn);
147 else
148 pwmd_free(conn);
151 /* Only called from libassuan after the BYE command. */
152 static void ssh_assuan_deinit(assuan_context_t ctx)
154 pwm_t *pwm = assuan_get_pointer(ctx);
156 if (pwm->tcp_conn) {
157 pwm->tcp_conn->fd = -1;
158 ssh_deinit(pwm->tcp_conn);
159 pwm->tcp_conn = NULL;
164 * Sets common options from both pwmd_ssh_connect() and
165 * pwmd_ssh_connect_async().
167 static gpg_error_t init_tcp_conn(pwmd_tcp_conn_t **dst, const char *host,
168 int port, const char *identity, const char *user,
169 const char *known_hosts, int get)
171 pwmd_tcp_conn_t *conn;
172 gpg_error_t rc = 0;
173 char *pwbuf = NULL;
175 if (get) {
176 if (!host || !*host)
177 return GPG_ERR_INV_ARG;
179 else {
180 if (!host || !*host || !identity || !*identity || !known_hosts ||
181 !*known_hosts)
182 return GPG_ERR_INV_ARG;
185 conn = pwmd_calloc(1, sizeof(pwmd_tcp_conn_t));
187 if (!conn)
188 return gpg_error_from_errno(ENOMEM);
190 conn->port = port == -1 ? 22 : port;
191 conn->host = pwmd_strdup(host);
193 if (!conn->host) {
194 rc = gpg_error_from_errno(ENOMEM);
195 goto fail;
198 if (!get) {
199 struct passwd pw;
201 pwbuf = _getpwuid(&pw);
203 if (!pwbuf) {
204 rc = gpg_error_from_errno(errno);
205 goto fail;
208 conn->username = pwmd_strdup(user ? user : pw.pw_name);
210 if (!conn->username) {
211 rc = gpg_error_from_errno(ENOMEM);
212 goto fail;
215 conn->identity = _expand_homedir((char *)identity, &pw);
217 if (!conn->identity) {
218 rc = gpg_error_from_errno(ENOMEM);
219 goto fail;
222 conn->identity_pub = pwmd_strdup_printf("%s.pub", conn->identity);
224 if (!conn->identity_pub) {
225 rc = gpg_error_from_errno(ENOMEM);
226 goto fail;
229 conn->known_hosts = _expand_homedir((char *)known_hosts, &pw);
231 if (!conn->known_hosts) {
232 rc = gpg_error_from_errno(ENOMEM);
233 goto fail;
236 pwmd_free(pwbuf);
239 *dst = conn;
240 return 0;
242 fail:
243 if (pwbuf)
244 pwmd_free(pwbuf);
246 _free_ssh_conn(conn);
247 return rc;
250 static gpg_error_t do_connect(pwm_t *pwm, int prot, void *addr)
252 struct sockaddr_in their_addr;
254 pwm->tcp_conn->fd = socket(prot, SOCK_STREAM, 0);
256 if (pwm->tcp_conn->fd == -1)
257 return gpg_error_from_syserror();
259 if (pwm->tcp_conn->async)
260 fcntl(pwm->tcp_conn->fd, F_SETFL, O_NONBLOCK);
262 pwm->cmd = ASYNC_CMD_CONNECT;
263 their_addr.sin_family = prot;
264 their_addr.sin_port = htons(pwm->tcp_conn->port);
265 their_addr.sin_addr = *((struct in_addr *)addr);
266 memset(their_addr.sin_zero, '\0', sizeof their_addr.sin_zero);
268 #ifdef WITH_LIBPTH
269 if (pth_connect(pwm->tcp_conn->fd, (struct sockaddr *)&their_addr,
270 sizeof(their_addr)) == -1)
271 #else
272 if (connect(pwm->tcp_conn->fd, (struct sockaddr *)&their_addr,
273 sizeof(their_addr)) == -1)
274 #endif
275 return gpg_error_from_syserror();
277 return 0;
280 static gpg_error_t ares_error_to_pwmd(int status)
282 if (status != ARES_SUCCESS)
283 warnx("%s", ares_strerror(status));
285 switch (status) {
286 case ARES_ENODATA:
287 case ARES_EFORMERR:
288 case ARES_ENOTFOUND:
289 return GPG_ERR_UNKNOWN_HOST;
290 case ARES_ESERVFAIL:
291 return GPG_ERR_EHOSTDOWN;
292 case ARES_ETIMEOUT:
293 return GPG_ERR_TIMEOUT;
294 case ARES_ENOMEM:
295 return gpg_error_from_errno(ENOMEM);
296 case ARES_ECONNREFUSED:
297 return GPG_ERR_ECONNREFUSED;
298 default:
299 /* FIXME ??? */
300 return GPG_ERR_EHOSTUNREACH;
303 return ARES_SUCCESS;
306 static void dns_resolve_cb(void *arg, int status, int timeouts,
307 unsigned char *abuf, int alen)
309 pwm_t *pwm = arg;
310 int rc;
311 struct hostent *he;
313 if (status == ARES_EDESTRUCTION)
314 return;
316 if (status != ARES_SUCCESS) {
317 pwm->tcp_conn->rc = ares_error_to_pwmd(status);
318 return;
321 /* Check for an IPv6 address first. */
322 if (pwm->prot == PWMD_IP_ANY || pwm->prot == PWMD_IPV6)
323 rc = ares_parse_aaaa_reply(abuf, alen, &he, NULL, NULL);
324 else
325 rc = ares_parse_a_reply(abuf, alen, &he, NULL, NULL);
327 if (rc != ARES_SUCCESS) {
328 if (pwm->prot != PWMD_IP_ANY || rc != ARES_ENODATA) {
329 pwm->tcp_conn->rc = ares_error_to_pwmd(status);
330 return;
333 rc = ares_parse_a_reply(abuf, alen, &he, NULL, NULL);
335 if (rc != ARES_SUCCESS) {
336 pwm->tcp_conn->rc = ares_error_to_pwmd(status);
337 return;
341 pwm->tcp_conn->he = he;
342 pwm->tcp_conn->rc = do_connect(pwm, he->h_addrtype, he->h_addr);
345 gpg_error_t _do_pwmd_ssh_connect_async(pwm_t *pwm, const char *host,
346 int port, const char *identity, const char *user,
347 const char *known_hosts, pwmd_async_cmd_t which)
349 pwmd_tcp_conn_t *conn;
350 gpg_error_t rc;
352 if (!pwm)
353 return GPG_ERR_INV_ARG;
355 if (pwm->cmd != ASYNC_CMD_NONE)
356 return GPG_ERR_ASS_NESTED_COMMANDS;
358 rc = init_tcp_conn(&conn, host, port, identity, user, known_hosts,
359 which == ASYNC_CMD_HOSTKEY ? 1 : 0);
361 if (rc)
362 return rc;
364 conn->async = 1;
365 pwm->tcp_conn = conn;
366 pwm->tcp_conn->cmd = which;
367 pwm->cmd = ASYNC_CMD_DNS;
368 pwm->state = ASYNC_PROCESS;
369 ares_init(&pwm->tcp_conn->chan);
370 ares_query(pwm->tcp_conn->chan, pwm->tcp_conn->host, ns_c_any, ns_t_any,
371 dns_resolve_cb, pwm);
372 return 0;
375 static void *ssh_malloc(size_t size, void **data)
377 return pwmd_malloc(size);
380 static void ssh_free(void *ptr, void **data)
382 pwmd_free(ptr);
385 static void *ssh_realloc(void *ptr, size_t size, void **data)
387 return pwmd_realloc(ptr, size);
390 static int verify_host_key(pwm_t *pwm)
392 FILE *fp = fopen(pwm->tcp_conn->known_hosts, "r");
393 char *buf, *p;
395 if (!fp)
396 return 1;
398 buf = pwmd_malloc(LINE_MAX);
400 if (!buf)
401 goto fail;
403 while ((p = fgets(buf, LINE_MAX, fp))) {
404 if (*p == '#' || isspace(*p))
405 continue;
407 if (p[strlen(p)-1] == '\n')
408 p[strlen(p)-1] = 0;
410 if (!strcmp(buf, pwm->tcp_conn->hostkey))
411 goto done;
414 fail:
415 if (buf)
416 pwmd_free(buf);
418 fclose(fp);
419 return 1;
421 done:
422 pwmd_free(buf);
423 fclose(fp);
424 return 0;
427 static gpg_error_t authenticate_ssh(pwm_t *pwm)
429 const char *fp = libssh2_hostkey_hash(pwm->tcp_conn->session,
430 LIBSSH2_HOSTKEY_HASH_SHA1);
431 char *userauth;
433 pwm->tcp_conn->hostkey = _to_hex(fp, 20);
435 if (!pwm->tcp_conn->hostkey)
436 return gpg_error_from_errno(ENOMEM);
438 if (pwm->tcp_conn->cmd == ASYNC_CMD_HOSTKEY)
439 return 0;
441 if (!fp || verify_host_key(pwm))
442 return GPG_ERR_BAD_CERT;
444 userauth = libssh2_userauth_list(pwm->tcp_conn->session,
445 pwm->tcp_conn->username, strlen(pwm->tcp_conn->username));
447 if (!userauth || !strstr(userauth, "publickey"))
448 return GPG_ERR_BAD_PIN_METHOD;
450 if (libssh2_userauth_publickey_fromfile(pwm->tcp_conn->session,
451 pwm->tcp_conn->username, pwm->tcp_conn->identity_pub,
452 pwm->tcp_conn->identity, NULL))
453 return GPG_ERR_BAD_SECKEY;
455 return 0;
458 gpg_error_t _setup_ssh_session(pwm_t *pwm)
460 assuan_context_t ctx;
461 struct assuan_io_hooks io_hooks = {read_hook, write_hook};
462 gpg_error_t rc;
464 pwm->tcp_conn->session = libssh2_session_init_ex(ssh_malloc, ssh_free,
465 ssh_realloc, NULL);
467 if (!pwm->tcp_conn->session) {
468 rc = gpg_error_from_errno(ENOMEM);
469 goto fail;
472 if (libssh2_session_startup(pwm->tcp_conn->session, pwm->tcp_conn->fd)) {
473 rc = GPG_ERR_ASSUAN_SERVER_FAULT;
474 goto fail;
477 rc = authenticate_ssh(pwm);
479 if (rc)
480 goto fail;
482 if (pwm->tcp_conn->cmd == ASYNC_CMD_HOSTKEY) {
483 pwm->result = pwmd_strdup(pwm->tcp_conn->hostkey);
485 if (!pwm->result) {
486 rc = gpg_error_from_errno(ENOMEM);
487 goto fail;
490 return 0;
493 pwm->tcp_conn->channel = libssh2_channel_open_session(pwm->tcp_conn->session);
495 if (!pwm->tcp_conn->channel) {
496 rc = GPG_ERR_ASSUAN_SERVER_FAULT;
497 goto fail;
500 if (libssh2_channel_shell(pwm->tcp_conn->channel)) {
501 rc = GPG_ERR_ASSUAN_SERVER_FAULT;
502 goto fail;
505 assuan_set_io_hooks(&io_hooks);
506 rc = assuan_socket_connect_fd(&ctx, pwm->tcp_conn->fd, 0, pwm);
508 if (rc)
509 goto fail;
511 assuan_set_finish_handler(ctx, ssh_assuan_deinit);
512 pwm->ctx = ctx;
513 rc = _connect_finalize(pwm);
515 if (rc)
516 goto fail;
518 return 0;
520 fail:
521 _free_ssh_conn(pwm->tcp_conn);
522 pwm->tcp_conn = NULL;
523 return gpg_err_code(rc);
526 gpg_error_t _do_pwmd_ssh_connect(pwm_t *pwm, const char *host, int port,
527 const char *identity, const char *user, const char *known_hosts, int get)
529 pwmd_tcp_conn_t *conn;
530 gpg_error_t rc;
532 if (!pwm)
533 return GPG_ERR_INV_ARG;
535 if (pwm->cmd != ASYNC_CMD_NONE)
536 return GPG_ERR_INV_STATE;
538 rc = init_tcp_conn(&conn, host, port, identity, user, known_hosts, get);
540 if (rc)
541 return rc;
543 pwm->tcp_conn = conn;
544 pwm->tcp_conn->cmd = get ? ASYNC_CMD_HOSTKEY : ASYNC_CMD_NONE;
545 pwm->cmd = ASYNC_CMD_DNS;
546 ares_init(&pwm->tcp_conn->chan);
547 ares_query(pwm->tcp_conn->chan, pwm->tcp_conn->host, ns_c_any, ns_t_any,
548 dns_resolve_cb, pwm);
550 /* dns_resolve_cb() may have already been called. */
551 if (pwm->tcp_conn->rc) {
552 rc = pwm->tcp_conn->rc;
553 goto fail;
557 * Fake a blocking DNS lookup. libcares does a better job than
558 * getaddrinfo().
560 do {
561 fd_set rfds, wfds;
562 int n;
563 struct timeval tv;
565 FD_ZERO(&rfds);
566 FD_ZERO(&wfds);
567 n = ares_fds(pwm->tcp_conn->chan, &rfds, &wfds);
568 ares_timeout(pwm->tcp_conn->chan, NULL, &tv);
569 #ifdef WITH_LIBPTH
570 n = pth_select(n, &rfds, &wfds, NULL, &tv);
571 #else
572 n = select(n, &rfds, &wfds, NULL, &tv);
573 #endif
575 if (n == -1) {
576 rc = gpg_error_from_syserror();
577 goto fail;
579 else if (n == 0) {
580 rc = GPG_ERR_TIMEOUT;
581 goto fail;
584 ares_process(pwm->tcp_conn->chan, &rfds, &wfds);
586 if (pwm->tcp_conn->rc)
587 break;
588 } while (pwm->cmd == ASYNC_CMD_DNS);
590 if (pwm->tcp_conn->rc) {
591 rc = pwm->tcp_conn->rc;
592 goto fail;
595 rc = _setup_ssh_session(pwm);
596 pwm->cmd = ASYNC_CMD_NONE;
598 if (pwm->tcp_conn)
599 pwm->tcp_conn->cmd = ASYNC_CMD_NONE;
601 fail:
602 return rc;
606 * ssh://[username@]hostname[:port],identity,known_hosts
608 * Any missing parameters are checked for in init_tcp_conn().
610 gpg_error_t _parse_ssh_url(char *str, char **host, int *port, char **user,
611 char **identity, char **known_hosts)
613 char *p;
614 char *t;
615 int len;
617 *host = *user = *identity = *known_hosts = NULL;
618 *port = -1;
619 p = strrchr(str, '@');
621 if (p) {
622 len = strlen(str)-strlen(p)+1;
623 *user = pwmd_malloc(len);
625 if (!*user)
626 return gpg_error_from_errno(ENOMEM);
628 snprintf(*user, len, "%s", str);
629 p++;
631 else
632 p = str;
634 t = strchr(p, ':');
636 if (t) {
637 len = strlen(p)-strlen(t)+1;
638 *host = pwmd_malloc(len);
640 if (!*host)
641 return gpg_error_from_errno(ENOMEM);
643 snprintf(*host, len, "%s", p);
644 t++;
645 *port = atoi(t);
647 while (*t && isdigit(*t))
648 t++;
650 p = t;
653 t = strchr(p, ',');
655 if (t) {
656 char *t2;
658 if (!*host) {
659 len = strlen(p)-strlen(t)+1;
660 *host = pwmd_malloc(len);
662 if (!*host)
663 return gpg_error_from_errno(ENOMEM);
665 snprintf(*host, len, "%s", p);
668 t++;
669 t2 = strchr(t, ',');
671 if (t2)
672 len = strlen(t)-strlen(t2)+1;
673 else
674 len = strlen(t)+1;
676 *identity = pwmd_malloc(len);
678 if (!*identity)
679 return gpg_error_from_errno(ENOMEM);
681 snprintf(*identity, len, "%s", t);
683 if (t2) {
684 t2++;
685 t += len+1;
686 len = strlen(t2)+1;
687 *known_hosts = pwmd_malloc(len);
689 if (!*known_hosts)
690 return gpg_error_from_errno(ENOMEM);
692 snprintf(*known_hosts, len, "%s", t2);
695 else {
696 if (!*host) {
697 len = strlen(p)+1;
698 *host = pwmd_malloc(len);
700 if (!*host)
701 return gpg_error_from_errno(ENOMEM);
703 snprintf(*host, len, "%s", p);
707 return 0;
710 void _ssh_disconnect(pwm_t *pwm)
712 ssh_deinit(pwm->tcp_conn);
713 pwm->tcp_conn = NULL;