diff options
-rw-r--r-- | CHANGES | 19 | ||||
-rw-r--r-- | RELEASE-NOTES | 2 | ||||
-rw-r--r-- | docs/libcurl/curl_easy_setopt.3 | 38 | ||||
-rw-r--r-- | include/curl/curl.h | 49 | ||||
-rw-r--r-- | lib/ssh.c | 157 | ||||
-rw-r--r-- | lib/strerror.c | 2 | ||||
-rw-r--r-- | lib/url.c | 27 | ||||
-rw-r--r-- | lib/urldata.h | 21 | ||||
-rw-r--r-- | src/main.c | 10 |
9 files changed, 311 insertions, 14 deletions
@@ -6,19 +6,26 @@ Changelog +Daniel Stenberg (23 Jul 2009) +- Added CURLOPT_SSH_KNOWNHOSTS, CURLOPT_SSH_KEYFUNCTION, CURLOPT_SSH_KEYDATA. + They introduce known_host support for SSH keys to libcurl. See docs for + details. Note that this feature depends on a new enough libssh2 version, to + be supported in libssh2 1.2 and later (or current git repo at this time). + Michal Marek (22 Jul 2009) - David Binderman found a memory and fd leak in lib/gtls.c:load_file() - (https://bugzilla.novell.com/523919). When looking at the code, I found - that also the ptr pointer can leak. + (https://bugzilla.novell.com/523919). When looking at the code, I found that + also the ptr pointer can leak. + Kamil Dudka (20 Jul 2009) -- Claes Jakobsson improved the support for client certificates handling - in NSS-powered libcurl. Now the client certificates can be selected +- Claes Jakobsson improved the support for client certificates handling in + NSS-powered libcurl. Now the client certificates can be selected automatically by a NSS built-in hook. Additionally pre-login to all PKCS11 slots is no more performed. It used to cause problems with HW tokens. - Fixed reference counting for NSS client certificates. Now the PEM reader - module should be always properly unloaded on Curl_nss_cleanup(). If the unload - fails though, libcurl will try to reuse the already loaded instance. + module should be always properly unloaded on Curl_nss_cleanup(). If the + unload fails though, libcurl will try to reuse the already loaded instance. Daniel Fandrich (15 Jul 2009) - Added nonblock.c to the non-automake makefiles (note that the dependencies diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 0f4da0b0e..0261cf882 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -10,6 +10,7 @@ Curl and libcurl 7.19.6 This release includes the following changes: o CURLOPT_FTPPORT (and curl's -P/--ftpport) support port ranges + o Added CURLOPT_SSH_KNOWNHOSTS, CURLOPT_SSH_KEYFUNCTION, CURLOPT_SSH_KEYDATA This release includes the following bugfixes: @@ -32,6 +33,7 @@ This release includes the following bugfixes: o curl -o - sends data to stdout using binary mode on windows o fixed the separators for "array" style string that CURLINFO_CERTINFO returns o auth problem over several hosts with re-used connection + o improved the support for client certificates in libcurl+NSS o fix leak in gtls code This release includes the following known bugs: diff --git a/docs/libcurl/curl_easy_setopt.3 b/docs/libcurl/curl_easy_setopt.3 index 94c1fc971..b30e04f06 100644 --- a/docs/libcurl/curl_easy_setopt.3 +++ b/docs/libcurl/curl_easy_setopt.3 @@ -1743,6 +1743,44 @@ Pass a char * pointing to a file name for your private key. If not used, libcurl defaults to using \fB~/.ssh/id_dsa\fP. If the file is password-protected, set the password with \fICURLOPT_KEYPASSWD\fP. (Added in 7.16.1) +.IP CURLOPT_SSH_KNOWNHOSTS +Pass a pointer to a zero terminated string holding the file name of the +known_host file to use. The known_hosts file should use the OpenSSH file +format as supported by libssh2. If this file is specified, libcurl will only +accept connections with hosts that are known and present in that file, with a +matching public key. Use \fICURLOPT_SSH_KEYFUNCTION\fP to alter the default +behavior on host and key (mis)matching. (Added in 7.19.6) +.IP CURLOPT_SSH_KEYFUNCTION +Pass a pointer to a curl_sshkeycallback function. It gets called when the +known_host matching has been done, to allow the application to act and decide +for libcurl how to proceed. It gets passed the CURL handle, the key from the +known_hosts file, the key from the remote site, info from libcurl on the +matching status and a custom pointer (set with \fICURLOPT_SSH_KEYDATA\fP). It +MUST return one of the following return codes to tell libcurl how to act: +.RS +.IP CURLKHSTAT_FINE_ADD_TO_FILE +The host+key is accepted and libcurl will append it to the known_hosts file +before continuing with the connection. This will also add the host+key combo +to the known_host pool kept in memory if it wasn't already present there. Note +that the adding of data to the file is done by completely replacing the file +with a new copy, so the permissions of the file must allow this. +.IP CURLKHSTAT_FINE +The host+key is accepted libcurl will continue with the connection. This will +also add the host+key combo to the known_host pool kept in memory if it wasn't +already present there. +.IP CURLKHSTAT_REJECT +The host+key is rejected. libcurl will deny the connection to continue and it +will be closed. +.IP CURLKHSTAT_DEFER +The host+key is rejected, but the SSH connection is asked to be kept alive. +This feature could be used when the app wants to somehow return back and act +on the host+key situation and then retry without needing the overhead of +setting it up from scratch again. +.RE + (Added in 7.19.6) +.IP CURLOPT_SSH_KEYDATA +Pass a void * as parameter. This pointer will be passed along verbatim to the +callback set with \fICURLOPT_SSH_KEYFUNCTION\fP. (Added in 7.19.6) .SH OTHER OPTIONS .IP CURLOPT_PRIVATE Pass a void * as parameter, pointing to data that should be associated with diff --git a/include/curl/curl.h b/include/curl/curl.h index 970c11678..d261ce39b 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -493,6 +493,45 @@ typedef enum { #define CURL_ERROR_SIZE 256 +struct curl_khkey { + const char *key; /* points to a zero-terminated string encoded with base64 + if len is zero, otherwise to the "raw" data */ + size_t len; + enum type { + CURLKHTYPE_UNKNOWN, + CURLKHTYPE_RSA1, + CURLKHTYPE_RSA, + CURLKHTYPE_DSS + } keytype; +}; + +/* this is the set of return values expected from the curl_sshkeycallback + callback */ +enum curl_khstat { + CURLKHSTAT_FINE_ADD_TO_FILE, + CURLKHSTAT_FINE, + CURLKHSTAT_REJECT, /* reject the connection, return an error */ + CURLKHSTAT_DEFER, /* do not accept it, but we can't answer right now so + this causes a CURLE_DEFER error but otherwise the + connection will be left intact etc */ + CURLKHSTAT_LAST /* not for use, only a marker for last-in-list */ +}; + +/* this is the set of status codes pass in to the callback */ +enum curl_khmatch { + CURLKHMATCH_OK, /* match */ + CURLKHMATCH_MISMATCH, /* host found, key mismatch! */ + CURLKHMATCH_MISSING, /* no matching host/key found */ + CURLKHMATCH_LAST /* not for use, only a marker for last-in-list */ +}; + +typedef int + (*curl_sshkeycallback) (CURL *easy, /* easy handle */ + const struct curl_khkey *knownkey, /* known */ + const struct curl_khkey *foundkey, /* found */ + enum curl_khmatch, /* libcurl's view on the keys */ + void *clientp); /* custom pointer passed from app */ + /* parameter for the CURLOPT_USE_SSL option */ typedef enum { CURLUSESSL_NONE, /* do not attempt to use SSL */ @@ -1214,6 +1253,16 @@ typedef enum { to all protocols except FILE and SCP. */ CINIT(REDIR_PROTOCOLS, LONG, 182), + /* set the SSH knownhost file name to use */ + CINIT(SSH_KNOWNHOSTS, OBJECTPOINT, 183), + + /* set the SSH host key callback, must point to a curl_sshkeycallback + function */ + CINIT(SSH_KEYFUNCTION, FUNCTIONPOINT, 184), + + /* set the SSH host key callback custom pointer */ + CINIT(SSH_KEYDATA, OBJECTPOINT, 185), + CURLOPT_LASTENTRY /* the last unused */ } CURLoption; @@ -306,6 +306,7 @@ static void state(struct connectdata *conn, sshstate nowstate) static const char * const names[] = { "SSH_STOP", "SSH_S_STARTUP", + "SSH_HOSTKEY", "SSH_AUTHLIST", "SSH_AUTH_PKEY_INIT", "SSH_AUTH_PKEY", @@ -433,6 +434,21 @@ static CURLcode ssh_getworkingpath(struct connectdata *conn, return CURLE_OK; } +static int sshkeycallback(CURL *easy, + const struct curl_khkey *knownkey, /* known */ + const struct curl_khkey *foundkey, /* found */ + enum curl_khmatch match, + void *clientp) +{ + (void)easy; + (void)knownkey; + (void)foundkey; + (void)clientp; + + /* we only allow perfect matches, and we reject everything else */ + return (match != CURLKHMATCH_OK)?CURLKHSTAT_REJECT:CURLKHSTAT_FINE; +} + /* * Earlier libssh2 versions didn't have the ability to seek to 64bit positions * with 32bit size_t. @@ -483,9 +499,15 @@ static CURLcode ssh_statemach_act(struct connectdata *conn, bool *block) break; } - /* Set libssh2 to non-blocking, since cURL is all non-blocking */ + /* Set libssh2 to non-blocking, since everything internally is + non-blocking */ libssh2_session_set_blocking(sshc->ssh_session, 0); + state(conn, SSH_HOSTKEY); + + /* fall-through */ + case SSH_HOSTKEY: + #ifdef CURL_LIBSSH2_DEBUG /* * Before we authenticate we should check the hostkey's fingerprint @@ -527,12 +549,121 @@ static CURLcode ssh_statemach_act(struct connectdata *conn, bool *block) } } +#ifdef HAVE_LIBSSH2_KNOWNHOST_API + if(data->set.str[STRING_SSH_KNOWNHOSTS]) { + /* we're asked to verify the host against a file */ + int keytype; + size_t keylen; + const char *remotekey = libssh2_session_hostkey(sshc->ssh_session, + &keylen, &keytype); + int keycheck; + int keybit; + + if(remotekey) { + /* + * A subject to figure out is what host name we need to pass in here. + * What host name does OpenSSH store in its file if an IDN name is + * used? + */ + struct libssh2_knownhost *host; + enum curl_khmatch keymatch; + curl_sshkeycallback func = + data->set.ssh_keyfunc?data->set.ssh_keyfunc:sshkeycallback; + struct curl_khkey knownkey; + struct curl_khkey *knownkeyp = NULL; + struct curl_khkey foundkey; + + keybit = (keytype == LIBSSH2_HOSTKEY_TYPE_RSA)? + LIBSSH2_KNOWNHOST_KEY_SSHRSA:LIBSSH2_KNOWNHOST_KEY_SSHDSS; + + keycheck = libssh2_knownhost_check(sshc->kh, + conn->host.name, + remotekey, keylen, + LIBSSH2_KNOWNHOST_TYPE_PLAIN| + LIBSSH2_KNOWNHOST_KEYENC_RAW| + keybit, + &host); + + infof(data, "SSH host check: %d, key: %s\n", keycheck, + (keycheck <= LIBSSH2_KNOWNHOST_CHECK_MISMATCH)? + host->key:"<none>"); + + /* setup 'knownkey' */ + if(keycheck <= LIBSSH2_KNOWNHOST_CHECK_MISMATCH) { + knownkey.key = host->key; + knownkey.len = 0; + knownkey.keytype = (keytype == LIBSSH2_HOSTKEY_TYPE_RSA)? + CURLKHTYPE_RSA : CURLKHTYPE_DSS; + knownkeyp = &knownkey; + } + + /* setup 'foundkey' */ + foundkey.key = remotekey; + foundkey.len = keylen; + foundkey.keytype = (keytype == LIBSSH2_HOSTKEY_TYPE_RSA)? + CURLKHTYPE_RSA : CURLKHTYPE_DSS; + + /* + * if any of the LIBSSH2_KNOWNHOST_CHECK_* defines and the + * curl_khmatch enum are ever modified, we need to introduce a + * translation table here! + */ + keymatch = (enum curl_khmatch)keycheck; + + /* Ask the callback how to behave */ + rc = func(data, knownkeyp, /* from the knownhosts file */ + &foundkey, /* from the remote host */ + keymatch, data->set.ssh_keyfunc_userp); + } + else + /* no remotekey means failure! */ + rc = CURLKHSTAT_REJECT; + + switch(rc) { + default: /* unknown return codes will equal reject */ + case CURLKHSTAT_REJECT: + state(conn, SSH_SESSION_FREE); + case CURLKHSTAT_DEFER: + /* DEFER means bail out but keep the SSH_HOSTKEY state */ + result = sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION; + break; + case CURLKHSTAT_FINE: + case CURLKHSTAT_FINE_ADD_TO_FILE: + /* proceed */ + if(keycheck != LIBSSH2_KNOWNHOST_CHECK_MATCH) { + /* the found host+key didn't match but has been told to be fine + anyway so we add it in memory */ + int addrc = libssh2_knownhost_add(sshc->kh, + conn->host.name, NULL, + remotekey, keylen, + LIBSSH2_KNOWNHOST_TYPE_PLAIN| + LIBSSH2_KNOWNHOST_KEYENC_RAW| + keybit, NULL); + if(addrc) + infof(data, "Warning adding the known host %s failed!\n", + conn->host.name); + else if(rc == CURLKHSTAT_FINE_ADD_TO_FILE) { + /* now we write the entire in-memory list of known hosts to the + known_hosts file */ + int wrc = + libssh2_knownhost_writefile(sshc->kh, + data->set.str[STRING_SSH_KNOWNHOSTS], + LIBSSH2_KNOWNHOST_FILE_OPENSSH); + if(wrc) { + infof(data, "Warning, writing %s failed!\n", + data->set.str[STRING_SSH_KNOWNHOSTS]); + } + } + } + break; + } + } +#endif /* HAVE_LIBSSH2_KNOWNHOST_API */ + state(conn, SSH_AUTHLIST); break; case SSH_AUTHLIST: - /* TBD - methods to check the host keys need to be done */ - /* * Figure out authentication methods * NB: As soon as we have provided a username to an openssh server we @@ -2278,6 +2409,26 @@ static CURLcode ssh_connect(struct connectdata *conn, bool *done) return CURLE_FAILED_INIT; } +#ifdef HAVE_LIBSSH2_KNOWNHOST_API + if(data->set.str[STRING_SSH_KNOWNHOSTS]) { + int rc; + ssh->kh = libssh2_knownhost_init(ssh->ssh_session); + if(!ssh->kh) { + /* eeek. TODO: free the ssh_session! */ + return CURLE_FAILED_INIT; + } + + /* read all known hosts from there */ + rc = libssh2_knownhost_readfile(ssh->kh, + data->set.str[STRING_SSH_KNOWNHOSTS], + LIBSSH2_KNOWNHOST_FILE_OPENSSH); + if(rc) { + infof(data, "Failed to read known hosts from %s\n", + data->set.str[STRING_SSH_KNOWNHOSTS]); + } + } +#endif /* HAVE_LIBSSH2_KNOWNHOST_API */ + #ifdef CURL_LIBSSH2_DEBUG libssh2_trace(ssh->ssh_session, ~0); infof(data, "SSH socket: %d\n", sock); diff --git a/lib/strerror.c b/lib/strerror.c index a45e1f181..75067a7d0 100644 --- a/lib/strerror.c +++ b/lib/strerror.c @@ -172,7 +172,7 @@ curl_easy_strerror(CURLcode error) return "Malformed telnet option"; case CURLE_PEER_FAILED_VERIFICATION: - return "SSL peer certificate or SSH md5 fingerprint was not OK"; + return "SSL peer certificate or SSH remote key was not OK"; case CURLE_GOT_NOTHING: return "Server returned nothing (no headers, no data)"; @@ -2169,6 +2169,8 @@ CURLcode Curl_setopt(struct SessionHandle *data, CURLoption option, data->set.ssl.sessionid = (bool)(0 != va_arg(param, long)); break; +#ifdef USE_LIBSSH2 + /* we only include SSH options if explicitly built to support SSH */ case CURLOPT_SSH_AUTH_TYPES: data->set.ssh_auth_types = va_arg(param, long); break; @@ -2196,6 +2198,31 @@ CURLcode Curl_setopt(struct SessionHandle *data, CURLoption option, result = setstropt(&data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5], va_arg(param, char *)); break; +#ifdef HAVE_LIBSSH2_KNOWNHOST_API + case CURLOPT_SSH_KNOWNHOSTS: + /* + * Store the file name to read known hosts from. + */ + result = setstropt(&data->set.str[STRING_SSH_KNOWNHOSTS], + va_arg(param, char *)); + break; + + case CURLOPT_SSH_KEYFUNCTION: + /* setting to NULL is fine since the ssh.c functions themselves will + then rever to use the internal default */ + data->set.ssh_keyfunc = va_arg(param, curl_sshkeycallback); + break; + + case CURLOPT_SSH_KEYDATA: + /* + * Custom client data to pass to the SSH keyfunc callback + */ + data->set.ssh_keyfunc_userp = va_arg(param, void *); + break; +#endif /* HAVE_LIBSSH2_KNOWNHOST_API */ + +#endif /* USE_LIBSSH2 */ + case CURLOPT_HTTP_TRANSFER_DECODING: /* * disable libcurl transfer encoding is used diff --git a/lib/urldata.h b/lib/urldata.h index 6a857174f..a48fc91b7 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -468,6 +468,7 @@ typedef enum { SSH_STOP = 0, /* do nothing state, stops the state machine */ SSH_S_STARTUP, /* Session startup, First state in SSH-CONNECT */ + SSH_HOSTKEY, /* verify hostkey */ SSH_AUTHLIST, SSH_AUTH_PKEY_INIT, SSH_AUTH_PKEY, @@ -525,7 +526,7 @@ typedef enum { Everything that is strictly related to a connection is banned from this struct. */ struct SSHPROTO { - char *path; /* the path we operate on */ + char *path; /* the path we operate on */ }; /* ssh_conn is used for struct connection-oriented data in the connectdata @@ -566,6 +567,12 @@ struct ssh_conn { LIBSSH2_SFTP_HANDLE *sftp_handle; int waitfor; /* current READ/WRITE bits to wait for */ int orig_waitfor; /* default READ/WRITE bits wait for */ + + /* note that HAVE_LIBSSH2_KNOWNHOST_API is a define set in the libssh2.h + header */ +#ifdef HAVE_LIBSSH2_KNOWNHOST_API + LIBSSH2_KNOWNHOSTS *kh; +#endif #endif /* USE_LIBSSH2 */ }; @@ -1366,15 +1373,12 @@ enum dupstring { STRING_SET_RANGE, /* range, if used */ STRING_SET_REFERER, /* custom string for the HTTP referer field */ STRING_SET_URL, /* what original URL to work on */ - STRING_SSH_PRIVATE_KEY, /* path to the private key file for auth */ - STRING_SSH_PUBLIC_KEY, /* path to the public key file for auth */ STRING_SSL_CAPATH, /* CA directory name (doesn't work on windows) */ STRING_SSL_CAFILE, /* certificate file to verify peer against */ STRING_SSL_CIPHER_LIST, /* list of ciphers to use */ STRING_SSL_EGDSOCKET, /* path to file containing the EGD daemon socket */ STRING_SSL_RANDOM_FILE, /* path to file containing "random" data */ STRING_USERAGENT, /* User-Agent string */ - STRING_SSH_HOST_PUBLIC_KEY_MD5, /* md5 of host public key in ascii hex */ STRING_SSL_CRLFILE, /* crl file to check certificate */ STRING_SSL_ISSUERCERT, /* issuer cert file to check certificate */ STRING_USERNAME, /* <username>, if used */ @@ -1383,6 +1387,12 @@ enum dupstring { STRING_PROXYPASSWORD, /* Proxy <password>, if used */ STRING_NOPROXY, /* List of hosts which should not use the proxy, if used */ +#ifdef USE_LIBSSH2 + STRING_SSH_PRIVATE_KEY, /* path to the private key file for auth */ + STRING_SSH_PUBLIC_KEY, /* path to the public key file for auth */ + STRING_SSH_HOST_PUBLIC_KEY_MD5, /* md5 of host public key in ascii hex */ + STRING_SSH_KNOWNHOSTS, /* file name of knownhosts file */ +#endif #if defined(HAVE_GSSAPI) || defined(USE_WINDOWS_SSPI) STRING_SOCKS5_GSSAPI_SERVICE, /* GSSAPI service name */ #endif @@ -1496,6 +1506,9 @@ struct UserDefined { 2 - the same but also allow MKD to fail once */ + curl_sshkeycallback ssh_keyfunc; /* key matching callback */ + void *ssh_keyfunc_userp; /* custom pointer to callback */ + /* Here follows boolean settings that define how to behave during this session. They are STATIC, set by libcurl users or at least initially and they don't change during operations. */ diff --git a/src/main.c b/src/main.c index 58e5bf960..aeaa58831 100644 --- a/src/main.c +++ b/src/main.c @@ -4694,6 +4694,16 @@ operate(struct Configurable *config, int argc, argv_item_t argv[]) my_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE); my_setopt(curl, CURLOPT_SSL_VERIFYHOST, 1); } + else { + char *home = homedir(); + char *file = aprintf("%s/%sssh/known_hosts", home, DOT_CHAR); + if(home && file) { + free(home); + my_setopt_str(curl, CURLOPT_SSH_KNOWNHOSTS, file); + } + else + return CURLE_OUT_OF_MEMORY; + } if(config->no_body || config->remote_time) { /* no body or use remote time */ |