From 0e2a4c54d8da833f8e0a305a00d38f9f21769d41 Mon Sep 17 00:00:00 2001 From: Ben Kibbey Date: Mon, 8 Jun 2009 21:34:03 -0400 Subject: [PATCH] Updated to use the new LIBSSH2_KNOWNHOST API from libssh2. This requires an OpenSSH formatted known hosts file. This also lets resuming a connection previously created with pwmd_get_hostkey() and pwmd_get_hostkey_async() work with the SSH connection functions. --- doc/libpwmd.3 | 101 +++++++++++++++++--- src/libpwmd.c | 30 ++++-- src/libpwmd.h.in | 97 ++++++++++++++----- src/pwmc.c | 29 +++++- src/ssh.c | 281 +++++++++++++++++++++++++++++++++++++++++-------------- src/types.h | 5 + 6 files changed, 429 insertions(+), 114 deletions(-) diff --git a/doc/libpwmd.3 b/doc/libpwmd.3 index edd1658d..e0374409 100644 --- a/doc/libpwmd.3 +++ b/doc/libpwmd.3 @@ -1,4 +1,4 @@ -.TH "libpwmd.h" 3 "9 May 2009" "Version 6.0.2" "libpwmd" \" -*- nroff -*- +.TH "libpwmd.h" 3 "6 Jun 2009" "Version 6.0.2" "libpwmd" \" -*- nroff -*- .ad l .nh .SH NAME @@ -8,11 +8,11 @@ libpwmd.h - an API for accessing pwmd libpwmd is a library making it easy for applications to use the pwmd server. Pwmd version 2.0 or later is required; either locally or remotely. .SH "SSH Details" .PP -A remote connection to a pwmd server is possible by using an SSH channel which spawns a shell and executes a proxy server that connects to the pwmd local UNIX domain socket. Authentication is done by using SSH public key (see \fBssh-keygen(1)\fP) authentication and verifying the host key against a local file containing SHA1 hashes of known hosts. It's a lot like how the standard OpenSSH does things only the known_hosts file is in a different format. +A remote connection to a pwmd server is possible by using an SSH channel which spawns a shell and executes a proxy server that connects to the pwmd local Unix Domain Socket. Authentication is done by using SSH public key (see \fBssh-keygen(1)\fP) authentication and verifying the host key against a local OpenSSH known hosts formatted file. .PP -The server hash can be had by using \fBpwmd_get_hostkey()\fP and storing the result in a file. This file is then used as the \fIknown_hosts\fP argument to the SSH connection functions. +The servers public key can be had by using \fBpwmd_get_hostkey()\fP and storing the result in a file or done automatially by using a callback function \fBpwmd_knownhost_cb_t\fP while connecting to the unknown host. .PP -Here's an example \fBauthorized_keys(5)\fP entry. The hash portion should be the same as the contents of the generated \fBssh-keygen(1)\fP \fIidentity.pub\fP file which is passed as a parameter to the SSH connection functions: +On the server side you'll need a proxy server to connect to the real pwmd server. Here's an example \fBauthorized_keys(5)\fP entry that will do this. The hash portion should be the same as the contents of the generated \fBssh-keygen(1)\fP \fIidentity.pub\fP file which is passed as a parameter to the SSH connection functions: .PP .PP .nf @@ -25,6 +25,11 @@ Here's an example \fBauthorized_keys(5)\fP entry. The hash portion should be the Only an SSH identity without a passphrase is supported. For now anyway. This is a limitation of libssh2 (version 1.1 as of this writing). .RE .PP +\fBVersion:\fP +.RS 4 +6.0.2 This was the last version to use SHA1 sums in the known hosts file. Later version use the OpenSSH format exclusively. +.RE +.PP \fBTodo\fP .RS 4 X11 port forwarding so a remote pinentry can use the local display. @@ -168,6 +173,9 @@ The following example will list the element tree of the data file specified in t .ti -1c .RI "typedef gpg_error_t(* \fBpwmd_inquire_cb_t\fP )(void *user, const char *cmd, gpg_error_t rc, char **data, size_t *len)" .br +.ti -1c +.RI "typedef gpg_error_t(* \fBpwmd_knownhost_cb_t\fP )(void *user, const char *host, const char *key, size_t len)" +.br .in -1c .SS "Enumerations" @@ -185,7 +193,7 @@ The following example will list the element tree of the data file specified in t .RI "enum \fBpwmd_pinentry_t\fP { \fBPWMD_PINENTRY_OPEN\fP, \fBPWMD_PINENTRY_OPEN_FAILED\fP, \fBPWMD_PINENTRY_SAVE\fP, \fBPWMD_PINENTRY_SAVE_CONFIRM\fP, \fBPWMD_PINENTRY_CONFIRM\fP, \fBPWMD_PINENTRY_DEFAULT\fP, \fBPWMD_PINENTRY_CLOSE\fP }" .br .ti -1c -.RI "enum \fBpwmd_option_t\fP { \fBPWMD_OPTION_PASSPHRASE_CB\fP, \fBPWMD_OPTION_PASSPHRASE_DATA\fP, \fBPWMD_OPTION_PASSPHRASE\fP, \fBPWMD_OPTION_PINENTRY_TRIES\fP, \fBPWMD_OPTION_PINENTRY_PATH\fP, \fBPWMD_OPTION_PINENTRY_TTY\fP, \fBPWMD_OPTION_PINENTRY_TERM\fP, \fBPWMD_OPTION_PINENTRY_DISPLAY\fP, \fBPWMD_OPTION_PINENTRY_TITLE\fP, \fBPWMD_OPTION_PINENTRY_PROMPT\fP, \fBPWMD_OPTION_PINENTRY_DESC\fP, \fBPWMD_OPTION_PINENTRY_LC_CTYPE\fP, \fBPWMD_OPTION_PINENTRY_LC_MESSAGES\fP, \fBPWMD_OPTION_PINENTRY_TIMEOUT\fP, \fBPWMD_OPTION_STATUS_CB\fP, \fBPWMD_OPTION_STATUS_DATA\fP, \fBPWMD_OPTION_IP_VERSION\fP }" +.RI "enum \fBpwmd_option_t\fP { \fBPWMD_OPTION_PASSPHRASE_CB\fP, \fBPWMD_OPTION_PASSPHRASE_DATA\fP, \fBPWMD_OPTION_PASSPHRASE\fP, \fBPWMD_OPTION_KNOWNHOST_CB\fP, \fBPWMD_OPTION_KNOWNHOST_DATA\fP, \fBPWMD_OPTION_PINENTRY_TRIES\fP, \fBPWMD_OPTION_PINENTRY_PATH\fP, \fBPWMD_OPTION_PINENTRY_TTY\fP, \fBPWMD_OPTION_PINENTRY_TERM\fP, \fBPWMD_OPTION_PINENTRY_DISPLAY\fP, \fBPWMD_OPTION_PINENTRY_TITLE\fP, \fBPWMD_OPTION_PINENTRY_PROMPT\fP, \fBPWMD_OPTION_PINENTRY_DESC\fP, \fBPWMD_OPTION_PINENTRY_LC_CTYPE\fP, \fBPWMD_OPTION_PINENTRY_LC_MESSAGES\fP, \fBPWMD_OPTION_PINENTRY_TIMEOUT\fP, \fBPWMD_OPTION_STATUS_CB\fP, \fBPWMD_OPTION_STATUS_DATA\fP, \fBPWMD_OPTION_IP_VERSION\fP }" .br .in -1c .SS "Functions" @@ -357,6 +365,39 @@ The sent data is processed line-per-line. The line is either newline terminated .RE .PP +.SS "\fBpwmd_knownhost_cb_t\fP" +.PP +When \fBPWMD_OPTION_KNOWNHOST_CB\fP is set and a the current connections host was not found in the known hosts file, then this callback function can be used to confirm the addition of the new hostkey to the known_hosts file. +.PP +\fBParameters:\fP +.RS 4 +\fIuser\fP User data which was set with \fBPWMD_OPTION_KNOWNHOST_DATA\fP. +.br +\fIhost\fP The hostname as passed to the connecting function. +.br +\fIkey\fP The raw hostkey. Note that this differs from the format returned from \fBpwmd_get_hostkey()\fP. +.br +\fIlen\fP The hostkey length. +.RE +.PP +\fBReturn values:\fP +.RS 4 +\fI0\fP Add the host key to the known hosts file. +.br +\fIGPG_ERR_NOT_CONFIRMED\fP Do not add the host key and abort the connection. +.RE +.PP +\fBNote:\fP +.RS 4 +If the known hosts file cannot be modified do to filesystem restrictions when trying to add the new host key, no error is returned. Instead the host key is added to the current connections host key cache and the connection is considered valid. +.RE +.PP +\fBSee also:\fP +.RS 4 +\fBSSH Details\fP +.RE +.PP + .SS "\fBpwmd_passphrase_cb_t\fP" .PP The value of the option \fBPWMD_OPTION_PASSPHRASE_CB\fP which is set with \fBpwmd_setopt()\fP. @@ -465,6 +506,26 @@ An empty string as the passphrase is allowed. .PP .TP +\fB\fIPWMD_OPTION_KNOWNHOST_CB \fP\fP +A function to confirm an unknown host hash which wasn't found in the known hosts file. +.PP +\fBSee also:\fP +.RS 4 +\fBSSH Details\fP +.RE +.PP + +.TP +\fB\fIPWMD_OPTION_KNOWNHOST_DATA \fP\fP +User supplied data which is passed to the known host function. +.PP +\fBSee also:\fP +.RS 4 +\fBSSH Details\fP +.RE +.PP + +.TP \fB\fIPWMD_OPTION_PINENTRY_TRIES \fP\fP An integer value that specifies the number of tries before \fBpinentry(1)\fP will give up when opening a file with the wrong supplied passphrase. The default is 3. .PP @@ -885,7 +946,7 @@ After returning, \fIn_fds\fP is set to the number of available file descriptors .SS "gpg_error_t pwmd_get_hostkey (\fBpwm_t\fP * pwm, const char * host, int port, char ** result)" .PP -Retrieve a remote SSH host key. +Retrieve a remote SSH public host key. .PP This key is needed for host verification of the remote pwmd server. You should be sure that the remote host is really the host that your wanting to connect to and not subject to a man-in-the-middle attack. .PP @@ -897,17 +958,22 @@ This key is needed for host verification of the remote pwmd server. You should b .br \fIport\fP The port or -1 for the default of 22. .br -\fIresult\fP The SHA1 sum of the server host key which must be freed with \fBpwmd_free()\fP. +\fIresult\fP An OpenSSH known hosts formatted line containing the servers public key which should be freed with \fBpwmd_free()\fP. .RE .PP \fBReturns:\fP .RS 4 -0 on success or an error code. +0 on success or an error code. +.RE +.PP +\fBVersion:\fP +.RS 4 +6.0.3 The connection is kept open but not verified. It can be resumed from one of the SSH connection functions. .RE .PP \fBSee also:\fP .RS 4 -\fBpwmd_get_hostkey_async()\fP, \fBSSH Details\fP +\fBpwmd_get_hostkey_async()\fP, \fBpwmd_ssh_connect()\fP, \fBSSH Details\fP .RE .PP @@ -930,12 +996,17 @@ This key is needed for host verification of the remote pwmd server. You should b .PP \fBReturns:\fP .RS 4 -0 on success or an error code. +0 on success or an error code. +.RE +.PP +\fBVersion:\fP +.RS 4 +6.0.3 The connection is kept open but not verified. It can be resumed from one of the SSH connection functions. .RE .PP \fBSee also:\fP .RS 4 -\fBpwmd_get_hostkey()\fP, \fBpwmd_process()\fP, \fBSSH Details\fP +\fBpwmd_get_hostkey()\fP, \fBpwmd_ssh_connect_async()\fP, \fBpwmd_process()\fP, \fBSSH Details\fP .RE .PP @@ -1388,7 +1459,7 @@ Connects to a pwmd server over an SSH channel. .RS 4 \fIpwm\fP A handle. .br -\fIhost\fP The hostname to connect to. +\fIhost\fP The hostname to connect to or NULL to resume a connection previously started with \fBpwmd_get_hostkey()\fP. .br \fIport\fP The port or -1 for the default of 22. .br @@ -1396,7 +1467,7 @@ Connects to a pwmd server over an SSH channel. .br \fIuser\fP The username on the SSH server to login as. If NULL then invoking username will be used. .br -\fIknown_hosts\fP A file containing the public SSH server key hash in SHA1 format which may be obtained with \fBpwmd_get_hostkey()\fP. +\fIknown_hosts\fP An OpenSSH known hosts formatted file containing public SSH server hashes which may be obtained with \fBpwmd_get_hostkey()\fP or via \fBpwmd_knownhost_cb_t\fP during a connection. .RE .PP \fBReturns:\fP @@ -1427,7 +1498,7 @@ This is a variant of \fBpwmd_ssh_connect()\fP that will not block while doing DN .RS 4 \fIpwm\fP A handle. .br -\fIhost\fP The hostname to connect to. +\fIhost\fP The hostname to connect to or NULL to resume a connection previously started with \fBpwmd_get_hostkey()\fP. .br \fIport\fP The port or -1 for the default of 22. .br @@ -1435,7 +1506,7 @@ This is a variant of \fBpwmd_ssh_connect()\fP that will not block while doing DN .br \fIuser\fP The username on the SSH server to login as. If NULL, the invoking username will be used. .br -\fIknown_hosts\fP A file containing the public SSH server key hash in SHA1 format which may be obtained with \fBpwmd_get_hostkey()\fP. +\fIknown_hosts\fP An OpenSSH known hosts formatted file containing public SSH server hashes which may be obtained with \fBpwmd_get_hostkey()\fP or via \fBpwmd_knownhost_cb_t\fP during a connection. .RE .PP \fBReturns:\fP diff --git a/src/libpwmd.c b/src/libpwmd.c index 0668d425..28d643c5 100644 --- a/src/libpwmd.c +++ b/src/libpwmd.c @@ -379,7 +379,6 @@ gpg_error_t pwmd_get_hostkey(pwm_t *pwm, const char *host, int port, #ifndef WITH_TCP return GPG_ERR_NOT_IMPLEMENTED; #else - char *hostkey; gpg_error_t rc; rc = _do_pwmd_ssh_connect(pwm, host, port, NULL, NULL, NULL, 1); @@ -387,12 +386,11 @@ gpg_error_t pwmd_get_hostkey(pwm_t *pwm, const char *host, int port, if (rc) return rc; - hostkey = pwmd_strdup(pwm->tcp_conn->hostkey); + *result = pwmd_strdup(pwm->tcp_conn->hostkey); - if (!hostkey) + if (!*result) rc = gpg_error_from_errno(ENOMEM); - *result = hostkey; return rc; #endif } @@ -678,6 +676,9 @@ static pwmd_async_t reset_async(pwm_t *pwm, int done) } #endif #ifdef WITH_TCP + if (pwm->tcp_conn) + pwm->tcp_conn->rc = 0; + if (done && pwm->tcp_conn) { _free_ssh_conn(pwm->tcp_conn); pwm->tcp_conn = NULL; @@ -871,7 +872,7 @@ pwmd_async_t pwmd_process(pwm_t *pwm, gpg_error_t *rc, char **result) return reset_async(pwm, 1); } - pwm->tcp_conn->state = SSH_INIT; + pwm->tcp_conn->state = SSH_NONE; pwm->tcp_conn->rc = 0; *rc = _setup_ssh_session(pwm); @@ -911,7 +912,10 @@ pwmd_async_t pwmd_process(pwm_t *pwm, gpg_error_t *rc, char **result) if (!*rc) { switch (pwm->tcp_conn->cmd) { case ASYNC_CMD_HOSTKEY: - *result = pwm->result; + *result = pwmd_strdup(pwm->tcp_conn->hostkey); + + if (!*result) + *rc = GPG_ERR_ENOMEM; break; default: break; @@ -1764,6 +1768,20 @@ gpg_error_t pwmd_setopt(pwm_t *pwm, pwmd_option_t opt, ...) rc = GPG_ERR_NOT_IMPLEMENTED; #endif break; + case PWMD_OPTION_KNOWNHOST_CB: +#ifdef WITH_TCP + pwm->kh_cb = va_arg(ap, pwmd_knownhost_cb_t); +#else + rc = GPG_ERR_NOT_IMPLEMENTED; +#endif + break; + case PWMD_OPTION_KNOWNHOST_DATA: +#ifdef WITH_TCP + pwm->kh_data = va_arg(ap, void *); +#else + rc = GPG_ERR_NOT_IMPLEMENTED; +#endif + break; default: rc = GPG_ERR_UNKNOWN_OPTION; break; diff --git a/src/libpwmd.h.in b/src/libpwmd.h.in index e1451a86..62930a76 100644 --- a/src/libpwmd.h.in +++ b/src/libpwmd.h.in @@ -26,20 +26,19 @@ * * A remote connection to a pwmd server is possible by using an SSH channel * which spawns a shell and executes a proxy server that connects to the pwmd - * local UNIX domain socket. Authentication is done by using SSH public key + * local Unix Domain Socket. Authentication is done by using SSH public key * (see \ref ssh-keygen(1)) authentication and verifying the host key against - * a local file containing SHA1 hashes of known hosts. It's a lot like how the - * standard OpenSSH does things only the known_hosts file is in a different - * format. + * a local OpenSSH known hosts formatted file. * - * The server hash can be had by using \ref pwmd_get_hostkey() and storing the - * result in a file. This file is then used as the \a known_hosts argument to - * the SSH connection functions. + * The servers public key can be had by using \ref pwmd_get_hostkey() and + * storing the result in a file or done automatially by using a callback + * function \ref pwmd_knownhost_cb_t while connecting to the unknown host. * - * Here's an example \ref authorized_keys(5) entry. The hash portion should be - * the same as the contents of the generated \ref ssh-keygen(1) \a - * identity.pub file which is passed as a parameter to the SSH connection - * functions: + * On the server side you'll need a proxy server to connect to the real pwmd + * server. Here's an example \ref authorized_keys(5) entry that will do this. + * The hash portion should be the same as the contents of the generated \ref + * ssh-keygen(1) \a identity.pub file which is passed as a parameter to the + * SSH connection functions: * * \code * command="socat UNIX-CONNECT:$HOME/.pwmd/socket -" ... @@ -48,6 +47,10 @@ * \note Only an SSH identity without a passphrase is supported. For now * anyway. This is a limitation of libssh2 (version 1.1 as of this writing). * + * \version 6.0.2 + * This was the last version to use SHA1 sums in the known hosts file. Later + * version use the OpenSSH format exclusively. + * * \x11 */ @@ -345,6 +348,28 @@ typedef int (*pwmd_status_cb_t)(void *user, const char *line); typedef gpg_error_t (*pwmd_inquire_cb_t)(void *user, const char *cmd, gpg_error_t rc, char **data, size_t *len); +/*! \typedef pwmd_knownhost_cb_t + * When \ref PWMD_OPTION_KNOWNHOST_CB is set and a the current connections + * host was not found in the known hosts file, then this callback function can + * be used to confirm the addition of the new hostkey to the known_hosts file. + * \param user User data which was set with \ref PWMD_OPTION_KNOWNHOST_DATA. + * \param host The hostname as passed to the connecting function. + * \param key The raw hostkey. Note that this differs from the format returned + * from \ref pwmd_get_hostkey(). + * \param len The hostkey length. + * \retval 0 Add the host key to the known hosts file. + * \retval GPG_ERR_NOT_CONFIRMED Do not add the host key and abort the + * connection. + * + * \note If the known hosts file cannot be modified do to filesystem + * restrictions when trying to add the new host key, no error is returned. + * Instead the host key is added to the current connections host key cache and + * the connection is considered verified. + * + * \see \ref ssh + */ +typedef gpg_error_t (*pwmd_knownhost_cb_t)(void *user, const char *host, + const char *key, size_t len); /*! \enum pwmd_option_t * @@ -373,6 +398,19 @@ typedef enum { */ PWMD_OPTION_PASSPHRASE, + /*! A function to confirm an unknown host hash which wasn't found in the + * known hosts file. + * + * \see \ref ssh + */ + PWMD_OPTION_KNOWNHOST_CB, + + /*! User supplied data which is passed to the known host function. + * + * \see \ref ssh + */ + PWMD_OPTION_KNOWNHOST_DATA, + /*! An integer value that specifies the number of tries before \ref * pinentry(1) will give up when opening a file with the wrong supplied * passphrase. The default is 3. @@ -500,15 +538,17 @@ gpg_error_t pwmd_connect(pwm_t *pwm, const char *path) * Connects to a pwmd server over an SSH channel. * * \param pwm A handle. - * \param host The hostname to connect to. + * \param host The hostname to connect to or NULL to resume a connection + * previously started with \ref pwmd_get_hostkey(). * \param port The port or -1 for the default of 22. * \param identity The SSH identity file to use for authentication. This * should specify the private key. The public key is assumed to be \a * identity.pub. * \param user The username on the SSH server to login as. If NULL then * invoking username will be used. - * \param known_hosts A file containing the public SSH server key hash in SHA1 - * format which may be obtained with \ref pwmd_get_hostkey(). + * \param known_hosts An OpenSSH known hosts formatted file containing public + * SSH server hashes which may be obtained with \ref pwmd_get_hostkey() or via + * \ref pwmd_knownhost_cb_t during a connection. * \return 0 on success or an error code. * \filepath * \see pwmd_ssh_connect_async(), \ref PWMD_OPTION_IP_VERSION, @@ -527,15 +567,17 @@ gpg_error_t pwmd_ssh_connect(pwm_t *pwm, const char *host, int port, * \process * * \param pwm A handle. - * \param host The hostname to connect to. + * \param host The hostname to connect to or NULL to resume a connection + * previously started with \ref pwmd_get_hostkey(). * \param port The port or -1 for the default of 22. * \param identity The SSH identity file to use for authentication. This * should specify the private key. The public key is assumed to be \a * identity.pub. * \param user The username on the SSH server to login as. If NULL, the * invoking username will be used. - * \param known_hosts A file containing the public SSH server key hash in SHA1 - * format which may be obtained with \ref pwmd_get_hostkey(). + * \param known_hosts An OpenSSH known hosts formatted file containing public + * SSH server hashes which may be obtained with \ref pwmd_get_hostkey() or via + * \ref pwmd_knownhost_cb_t during a connection. * \return 0 on success or an error code. * \filepath * \see pwmd_process(), \ref PWMD_OPTION_IP_VERSION, pwmd_disconnect(), @@ -602,7 +644,7 @@ gpg_error_t pwmd_connect_url_async(pwm_t *pwm, const char *url) __attribute__ ((warn_unused_result)); -/*! \brief Retrieve a remote SSH host key. +/*! \brief Retrieve a remote SSH public host key. * * This key is needed for host verification of the remote pwmd server. You * should be sure that the remote host is really the host that your wanting to @@ -611,10 +653,15 @@ gpg_error_t pwmd_connect_url_async(pwm_t *pwm, const char *url) * \param pwm A handle. * \param host The hostname to connect to. * \param port The port or -1 for the default of 22. - * \param[out] result The SHA1 sum of the server host key which must be freed - * with \ref pwmd_free(). + * \param[out] result An OpenSSH known hosts formatted line containing the + * servers public key which should be freed with \ref pwmd_free(). * \return 0 on success or an error code. - * \see pwmd_get_hostkey_async(), \ref ssh + * + * \version 6.0.3 + * The connection is kept open but not verified. It can be resumed from one of + * the SSH connection functions. + * + * \see pwmd_get_hostkey_async(), pwmd_ssh_connect(), \ref ssh */ gpg_error_t pwmd_get_hostkey(pwm_t *pwm, const char *host, int port, char **result) @@ -633,7 +680,13 @@ gpg_error_t pwmd_get_hostkey(pwm_t *pwm, const char *host, int port, * \param host The hostname to connect to. * \param port The port or -1 for the default of 22. * \return 0 on success or an error code. - * \see pwmd_get_hostkey(), \ref pwmd_process(), \ref ssh + * + * \version 6.0.3 + * The connection is kept open but not verified. It can be resumed from one of + * the SSH connection functions. + * + * \see pwmd_get_hostkey(), pwmd_ssh_connect_async(), \ref pwmd_process(), + * \ref ssh */ gpg_error_t pwmd_get_hostkey_async(pwm_t *pwm, const char *host, int port) __attribute__ ((warn_unused_result)); diff --git a/src/pwmc.c b/src/pwmc.c index fedc572a..ae0004d5 100644 --- a/src/pwmc.c +++ b/src/pwmc.c @@ -266,6 +266,21 @@ static gpg_error_t process_cmd(pwm_t *pwm, char **result, int input) return rc; } +static gpg_error_t knownhost_cb(void *data, const char *host, const char *key, + size_t len) +{ + gpg_error_t rc; + char *buf = pwmd_strdup_printf(N_("Password Manager Daemon: %s\n\nWhile attepting an SSH connection to %s, there was a problem verifying it's hostkey against the list of known and trusted hosts file. Would you like to treat this connection as trusted for this and future connections?"), (char *)data, host); + + rc = pwmd_setopt(pwm, PWMD_OPTION_PINENTRY_TITLE, buf); + pwmd_free(buf); + + if (rc) + return rc; + + return pwmd_getpin(pwm, NULL, NULL, PWMD_PINENTRY_CONFIRM); +} + int main(int argc, char *argv[]) { int opt; @@ -559,6 +574,16 @@ int main(int argc, char *argv[]) goto done; } + error = pwmd_setopt(pwm, PWMD_OPTION_KNOWNHOST_CB, knownhost_cb); + + if (error) + goto done; + + error = pwmd_setopt(pwm, PWMD_OPTION_KNOWNHOST_DATA, clientname); + + if (error) + goto done; + #ifdef DEBUG if (method >= 2) { if (get) { @@ -574,7 +599,7 @@ int main(int argc, char *argv[]) if (error) goto done; - printf("%s\n", hostkey); + printf("%s", hostkey); pwmd_free(hostkey); pwmd_free(password); pwmd_close(pwm); @@ -605,7 +630,7 @@ int main(int argc, char *argv[]) if (error) goto done; - printf("%s\n", hostkey); + printf("%s", hostkey); pwmd_free(hostkey); pwmd_free(password); pwmd_close(pwm); diff --git a/src/ssh.c b/src/ssh.c index 6078e21c..0a22ce1d 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -168,33 +168,41 @@ static void ssh_assuan_deinit(assuan_context_t ctx) */ static gpg_error_t init_tcp_conn(pwmd_tcp_conn_t **dst, const char *host, int port, const char *identity, const char *user, - const char *known_hosts, int get) + const char *known_hosts, int get, int resume) { - pwmd_tcp_conn_t *conn; + pwmd_tcp_conn_t *conn = *dst; gpg_error_t rc = 0; char *pwbuf = NULL; if (get) { + if (resume) { + if (host) + return GPG_ERR_INV_STATE; + + return 0; + } + if (!host || !*host) return GPG_ERR_INV_ARG; } - else { + else if (!resume) { if (!host || !*host || !identity || !*identity || !known_hosts || !*known_hosts) return GPG_ERR_INV_ARG; } + else if (resume) { + if (host) + return GPG_ERR_INV_STATE; - conn = pwmd_calloc(1, sizeof(pwmd_tcp_conn_t)); - - if (!conn) - return gpg_error_from_errno(ENOMEM); + if (!identity || !*identity || !known_hosts || !*known_hosts) + return GPG_ERR_INV_ARG; + } - conn->port = port == -1 ? 22 : port; - conn->host = pwmd_strdup(host); + if (!resume) { + conn = pwmd_calloc(1, sizeof(pwmd_tcp_conn_t)); - if (!conn->host) { - rc = gpg_error_from_errno(ENOMEM); - goto fail; + if (!conn) + return gpg_error_from_errno(ENOMEM); } if (!get) { @@ -207,6 +215,9 @@ static gpg_error_t init_tcp_conn(pwmd_tcp_conn_t **dst, const char *host, goto fail; } + if (conn->username) + pwmd_free(conn->username); + conn->username = pwmd_strdup(user ? user : pw.pw_name); if (!conn->username) { @@ -214,6 +225,9 @@ static gpg_error_t init_tcp_conn(pwmd_tcp_conn_t **dst, const char *host, goto fail; } + if (conn->identity) + pwmd_free(conn->identity); + conn->identity = _expand_homedir((char *)identity, &pw); if (!conn->identity) { @@ -221,6 +235,9 @@ static gpg_error_t init_tcp_conn(pwmd_tcp_conn_t **dst, const char *host, goto fail; } + if (conn->identity_pub) + pwmd_free(conn->identity_pub); + conn->identity_pub = pwmd_strdup_printf("%s.pub", conn->identity); if (!conn->identity_pub) { @@ -228,6 +245,9 @@ static gpg_error_t init_tcp_conn(pwmd_tcp_conn_t **dst, const char *host, goto fail; } + if (conn->known_hosts) + pwmd_free(conn->known_hosts); + conn->known_hosts = _expand_homedir((char *)known_hosts, &pw); if (!conn->known_hosts) { @@ -238,7 +258,18 @@ static gpg_error_t init_tcp_conn(pwmd_tcp_conn_t **dst, const char *host, pwmd_free(pwbuf); } - *dst = conn; + if (!resume) { + conn->port = port == -1 ? 22 : port; + conn->host = pwmd_strdup(host); + + if (!conn->host) { + rc = gpg_error_from_errno(ENOMEM); + goto fail; + } + + *dst = conn; + } + return 0; fail: @@ -350,6 +381,7 @@ gpg_error_t _do_pwmd_ssh_connect_async(pwm_t *pwm, const char *host, { pwmd_tcp_conn_t *conn; gpg_error_t rc; + int resume = 0; if (!pwm) return GPG_ERR_INV_ARG; @@ -357,8 +389,15 @@ gpg_error_t _do_pwmd_ssh_connect_async(pwm_t *pwm, const char *host, if (pwm->cmd != ASYNC_CMD_NONE) return GPG_ERR_ASS_NESTED_COMMANDS; + /* Resume an existing connection that may have been started from + * pwmd_get_hostkey(). */ + if (pwm->tcp_conn) { + resume = 1; + conn = pwm->tcp_conn; + } + rc = init_tcp_conn(&conn, host, port, identity, user, known_hosts, - which == ASYNC_CMD_HOSTKEY ? 1 : 0); + which == ASYNC_CMD_HOSTKEY ? 1 : 0, resume); if (rc) return rc; @@ -366,12 +405,24 @@ gpg_error_t _do_pwmd_ssh_connect_async(pwm_t *pwm, const char *host, conn->async = 1; pwm->tcp_conn = conn; pwm->tcp_conn->cmd = which; - pwm->cmd = ASYNC_CMD_DNS; + pwm->cmd = resume ? ASYNC_CMD_CONNECT : ASYNC_CMD_DNS; pwm->state = ASYNC_PROCESS; - ares_init(&pwm->tcp_conn->chan); - ares_query(pwm->tcp_conn->chan, pwm->tcp_conn->host, ns_c_any, ns_t_any, - dns_resolve_cb, pwm); - return 0; + + if (!resume) { + ares_init(&pwm->tcp_conn->chan); + ares_query(pwm->tcp_conn->chan, pwm->tcp_conn->host, ns_c_any, + ns_t_any, dns_resolve_cb, pwm); + } + else { + /* There may not be any pending data waiting to be read from the SSH + * FD so resume the connection here instead of from pwmd_process(). */ + rc = _setup_ssh_session(pwm); + + if (rc == GPG_ERR_EAGAIN) + rc = 0; + } + + return rc; } static void *ssh_malloc(size_t size, void **data) @@ -389,43 +440,6 @@ static void *ssh_realloc(void *ptr, size_t size, void **data) return pwmd_realloc(ptr, size); } -static int verify_host_key(pwm_t *pwm) -{ - FILE *fp = fopen(pwm->tcp_conn->known_hosts, "r"); - char *buf, *p; - - if (!fp) - return 1; - - buf = pwmd_malloc(LINE_MAX); - - if (!buf) - goto fail; - - while ((p = fgets(buf, LINE_MAX, fp))) { - if (*p == '#' || isspace(*p)) - continue; - - if (p[strlen(p)-1] == '\n') - p[strlen(p)-1] = 0; - - if (!strcmp(buf, pwm->tcp_conn->hostkey)) - goto done; - } - -fail: - if (buf) - pwmd_free(buf); - - fclose(fp); - return 1; - -done: - pwmd_free(buf); - fclose(fp); - return 0; -} - gpg_error_t _setup_ssh_auth(pwm_t *pwm) { int n; @@ -468,28 +482,136 @@ gpg_error_t _setup_ssh_authlist(pwm_t *pwm) return _setup_ssh_auth(pwm); } +static gpg_error_t check_known_hosts(pwm_t *pwm) +{ + size_t len; + const char *key = libssh2_session_hostkey(pwm->tcp_conn->session, &len); + gpg_error_t rc = 0; + int n; + struct libssh2_knownhost *kh; + + while (!libssh2_knownhost_get(pwm->tcp_conn->kh, &kh, NULL)) + libssh2_knownhost_del(pwm->tcp_conn->kh, kh); + + n = libssh2_knownhost_readfile(pwm->tcp_conn->kh, + pwm->tcp_conn->known_hosts, LIBSSH2_KNOWNHOST_FILE_OPENSSH); + + if (n < 0 && pwm->tcp_conn->cmd != ASYNC_CMD_HOSTKEY) + return GPG_ERR_BAD_CERT; + + n = libssh2_knownhost_check(pwm->tcp_conn->kh, pwm->tcp_conn->host, + (char *)key, len, + LIBSSH2_KNOWNHOST_TYPE_PLAIN|LIBSSH2_KNOWNHOST_KEYENC_RAW, + &pwm->tcp_conn->hostent); + + switch (n) { + case LIBSSH2_KNOWNHOST_CHECK_MATCH: + break; + case LIBSSH2_KNOWNHOST_CHECK_NOTFOUND: + if (pwm->tcp_conn->cmd != ASYNC_CMD_HOSTKEY) { + if (!pwm->kh_cb) + rc = GPG_ERR_MISSING_CERT; + else + rc = pwm->kh_cb(pwm->kh_data, pwm->tcp_conn->host, key, + len); + + if (rc) + return rc; + } + + libssh2_knownhost_add(pwm->tcp_conn->kh, pwm->tcp_conn->host, NULL, + key, len, + LIBSSH2_KNOWNHOST_TYPE_PLAIN | + LIBSSH2_KNOWNHOST_KEYENC_RAW | + LIBSSH2_KNOWNHOST_KEY_SSHRSA, + &pwm->tcp_conn->hostent); + + /* It's not an error if writing the new host file fails since + * there isn't a way to notify the user. The hostkey is still + * valid though. */ + if (pwm->tcp_conn->cmd != ASYNC_CMD_HOSTKEY) { + char *tmp = tempnam(NULL, "khost"); + + if (!tmp) + return 0; + + if (!libssh2_knownhost_writefile(pwm->tcp_conn->kh, tmp, + LIBSSH2_KNOWNHOST_FILE_OPENSSH)) { + char *buf; + FILE *ifp, *ofp; + + buf = pwmd_malloc(LINE_MAX); + + if (!buf) { + free(tmp); + return 0; + } + + ifp = fopen(tmp, "r"); + ofp = fopen(pwm->tcp_conn->known_hosts, "w+"); + + while ((fgets(buf, LINE_MAX, ifp))) { + if (fprintf(ofp, buf, strlen(buf)) < 0) + break; + } + + fclose(ifp); + fclose(ofp); + pwmd_free(buf); + } + + free(tmp); + } + + return 0; + case LIBSSH2_KNOWNHOST_CHECK_MISMATCH: + case LIBSSH2_KNOWNHOST_CHECK_FAILURE: + return GPG_ERR_BAD_CERT; + } + + return 0; +} + static gpg_error_t verify_hostkey(pwm_t *pwm) { - const char *fp = libssh2_hostkey_hash(pwm->tcp_conn->session, - LIBSSH2_HOSTKEY_HASH_SHA1); + gpg_error_t rc; + size_t outlen; + char *buf; + + if (!pwm->tcp_conn->kh) + pwm->tcp_conn->kh = libssh2_knownhost_init(pwm->tcp_conn->session); - pwm->tcp_conn->hostkey = _to_hex(fp, 20); + if (!pwm->tcp_conn->kh) + return GPG_ERR_ENOMEM; - if (!pwm->tcp_conn->hostkey) + rc = check_known_hosts(pwm); + + if (rc) + return rc; + + buf = pwmd_malloc(LINE_MAX); + + if (!buf) return gpg_error_from_errno(ENOMEM); - if (pwm->tcp_conn->cmd == ASYNC_CMD_HOSTKEY) { - pwm->result = pwmd_strdup(pwm->tcp_conn->hostkey); + if (libssh2_knownhost_writeline(pwm->tcp_conn->kh, pwm->tcp_conn->hostent, + buf, LINE_MAX, &outlen, LIBSSH2_KNOWNHOST_FILE_OPENSSH)) { + pwmd_free(buf); + return gpg_error_from_errno(ENOMEM); + } - if (!pwm->result) - return gpg_error_from_errno(ENOMEM); + if (pwm->tcp_conn->hostkey) + pwmd_free(pwm->tcp_conn->hostkey); + + pwm->tcp_conn->hostkey = buf; + if (pwm->tcp_conn->cmd == ASYNC_CMD_HOSTKEY) { + libssh2_knownhost_del(pwm->tcp_conn->kh, pwm->tcp_conn->hostent); + pwm->tcp_conn->hostent = NULL; + pwm->tcp_conn->state = SSH_RESUME; return 0; } - if (!fp || verify_host_key(pwm)) - return GPG_ERR_BAD_CERT; - return _setup_ssh_authlist(pwm); } @@ -568,6 +690,11 @@ gpg_error_t _setup_ssh_init(pwm_t *pwm) { int n; + /* Resuming an SSH connection which may have been initially created with + * pwmd_get_hostkey(). */ + if (pwm->tcp_conn->state == SSH_RESUME) + goto done; + pwm->tcp_conn->state = SSH_INIT; n = libssh2_session_startup(pwm->tcp_conn->session, pwm->tcp_conn->fd); @@ -579,6 +706,7 @@ gpg_error_t _setup_ssh_init(pwm_t *pwm) return GPG_ERR_ASSUAN_SERVER_FAULT; } +done: return verify_hostkey(pwm); } @@ -586,7 +714,8 @@ gpg_error_t _setup_ssh_session(pwm_t *pwm) { gpg_error_t rc; - pwm->tcp_conn->session = libssh2_session_init_ex(ssh_malloc, ssh_free, + if (!pwm->tcp_conn->session) + pwm->tcp_conn->session = libssh2_session_init_ex(ssh_malloc, ssh_free, ssh_realloc, NULL); if (!pwm->tcp_conn->session) { @@ -608,6 +737,7 @@ gpg_error_t _do_pwmd_ssh_connect(pwm_t *pwm, const char *host, int port, { pwmd_tcp_conn_t *conn; gpg_error_t rc; + int resume = 0; if (!pwm) return GPG_ERR_INV_ARG; @@ -615,13 +745,25 @@ gpg_error_t _do_pwmd_ssh_connect(pwm_t *pwm, const char *host, int port, if (pwm->cmd != ASYNC_CMD_NONE) return GPG_ERR_INV_STATE; - rc = init_tcp_conn(&conn, host, port, identity, user, known_hosts, get); + if (pwm->tcp_conn) { + pwm->tcp_conn->async = 0; + resume = 1; + conn = pwm->tcp_conn; + } + + rc = init_tcp_conn(&conn, host, port, identity, user, known_hosts, get, + resume); if (rc) return rc; pwm->tcp_conn = conn; pwm->tcp_conn->cmd = get ? ASYNC_CMD_HOSTKEY : ASYNC_CMD_NONE; + pwm->cmd = ASYNC_CMD_NONE; + + if (resume) + goto done; + pwm->cmd = ASYNC_CMD_DNS; ares_init(&pwm->tcp_conn->chan); ares_query(pwm->tcp_conn->chan, pwm->tcp_conn->host, ns_c_any, ns_t_any, @@ -672,6 +814,7 @@ gpg_error_t _do_pwmd_ssh_connect(pwm_t *pwm, const char *host, int port, goto fail; } +done: rc = _setup_ssh_session(pwm); pwm->cmd = ASYNC_CMD_NONE; diff --git a/src/types.h b/src/types.h index 0be2eb05..aea3dac2 100644 --- a/src/types.h +++ b/src/types.h @@ -64,6 +64,7 @@ typedef enum { #ifdef WITH_TCP typedef enum { + SSH_RESUME = -1, SSH_NONE, SSH_INIT, SSH_AUTHLIST, @@ -86,6 +87,8 @@ typedef struct { struct hostent *he; LIBSSH2_SESSION *session; LIBSSH2_CHANNEL *channel; + LIBSSH2_KNOWNHOSTS *kh; + struct libssh2_knownhost *hostent; char *hostkey; pwmd_async_cmd_t cmd; pwmd_ssh_async_t state; @@ -97,6 +100,8 @@ struct pwm_s { #ifdef WITH_TCP pwmd_tcp_conn_t *tcp_conn; pwmd_ip_version_t prot; + pwmd_knownhost_cb_t kh_cb; + void *kh_data; #endif int fd; pwmd_async_t state; -- 2.11.4.GIT