From 74c2b4b8a62d2a6461b1535192e3c5e828f49c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Tue, 1 Nov 2022 12:54:29 +0100 Subject: [PATCH 1/4] ssh: verify the remote's host key against known_hosts if it exists It turns out this has been available in libssh2 for a long time and we should have been verifying this the whole time. --- src/libgit2/transports/ssh.c | 309 +++++++++++++++++++++++++---------- 1 file changed, 222 insertions(+), 87 deletions(-) diff --git a/src/libgit2/transports/ssh.c b/src/libgit2/transports/ssh.c index 89f085230..185d53959 100644 --- a/src/libgit2/transports/ssh.c +++ b/src/libgit2/transports/ssh.c @@ -454,6 +454,226 @@ static int _git_ssh_session_create( return 0; } +/* + * Returns the typemask argument to pass to libssh2_knownhost_check{,p} based on + * the type of key that libssh2_session_hostkey returns. + */ +static int fingerprint_type_mask(int keytype) +{ + int mask = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW; + return mask; + + switch (keytype) { + case LIBSSH2_HOSTKEY_TYPE_RSA: + mask |= LIBSSH2_KNOWNHOST_KEY_SSHRSA; + break; + case LIBSSH2_HOSTKEY_TYPE_DSS: + mask |= LIBSSH2_KNOWNHOST_KEY_SSHDSS; + break; +#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_256 + case LIBSSH2_HOSTKEY_TYPE_ECDSA_256: + mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_256; + break; + case LIBSSH2_HOSTKEY_TYPE_ECDSA_384: + mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_384; + break; + case LIBSSH2_HOSTKEY_TYPE_ECDSA_521: + mask |= LIBSSH2_KNOWNHOST_KEY_ECDSA_521; + break; +#endif +#ifdef LIBSSH2_HOSTKEY_TYPE_ED25519 + case LIBSSH2_HOSTKEY_TYPE_ED25519: + mask |= LIBSSH2_KNOWNHOST_KEY_ED25519; + break; +#endif + } + + return mask; +} + +#define KNOWN_HOSTS_FILE ".ssh/known_hosts" + +/* + * Check the host against the user's known_hosts file. + * + * Returns 1/0 for valid/''not-valid or <0 for an error + */ +static int check_against_known_hosts( + LIBSSH2_SESSION *session, + const char *hostname, + int port, + const char *key, + size_t key_len, + int key_type) +{ + int error, check, typemask, ret = 0; + git_str path = GIT_STR_INIT, home = GIT_STR_INIT; + LIBSSH2_KNOWNHOSTS *known_hosts = NULL; + struct libssh2_knownhost *host = NULL; + + if ((error = git__getenv(&home, "HOME")) < 0) { + return error; + } + + if ((error = git_str_joinpath(&path, git_str_cstr(&home), KNOWN_HOSTS_FILE)) < 0) { + ret = error; + goto out; + } + + if ((known_hosts = libssh2_knownhost_init(session)) == NULL) { + ssh_error(session, "error initializing known hosts"); + ret = -1; + goto out; + } + + /* + * Try to read the file and consider not finding it as not trusting the + * host rather than an error. + */ + error = libssh2_knownhost_readfile(known_hosts, git_str_cstr(&path), LIBSSH2_KNOWNHOST_FILE_OPENSSH); + if (error == LIBSSH2_ERROR_FILE) { + ret = 0; + goto out; + } + if (error < 0) { + ssh_error(session, "error reading known_hosts"); + ret = -1; + goto out; + } + + typemask = fingerprint_type_mask(key_type); + check = libssh2_knownhost_checkp(known_hosts, hostname, port, key, key_len, typemask, &host); + if (check == LIBSSH2_KNOWNHOST_CHECK_FAILURE) { + ssh_error(session, "error checking for known host"); + ret = -1; + goto out; + } + + ret = check == LIBSSH2_KNOWNHOST_CHECK_MATCH ? 1 : 0; + +out: + libssh2_knownhost_free(known_hosts); + git_str_clear(&path); + git_str_clear(&home); + + return ret; +} + +/* + * Perform the check for the session's certificate against known hosts if + * possible and then ask the user if they have a callback. + * + * Returns 1/0 for valid/not-valid or <0 for an error + */ +static int check_certificate( + LIBSSH2_SESSION *session, + git_transport_certificate_check_cb check_cb, + void *check_cb_payload, + const char *host, + const char *portstr) +{ + git_cert_hostkey cert = {{ 0 }}; + const char *key; + size_t cert_len; + int cert_type, port, cert_valid = 0, error = 0; + + if ((key = libssh2_session_hostkey(session, &cert_len, &cert_type)) == NULL) { + ssh_error(session, "failed to retrieve hostkey"); + return -1; + } + + /* Try to parse the port as a number, if we can't then fall back to default */ + if (git__strntol32(&port, portstr, strlen(portstr), NULL, 10) < 0) + port = -1; + + if ((cert_valid = check_against_known_hosts(session, host, port, key, cert_len, cert_type)) < 0) + return -1; + + cert.parent.cert_type = GIT_CERT_HOSTKEY_LIBSSH2; + if (key != NULL) { + cert.type |= GIT_CERT_SSH_RAW; + cert.hostkey = key; + cert.hostkey_len = cert_len; + switch (cert_type) { + case LIBSSH2_HOSTKEY_TYPE_RSA: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_RSA; + break; + case LIBSSH2_HOSTKEY_TYPE_DSS: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_DSS; + break; + +#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_256 + case LIBSSH2_HOSTKEY_TYPE_ECDSA_256: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_256; + break; + case LIBSSH2_HOSTKEY_TYPE_ECDSA_384: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_384; + break; + case LIBSSH2_KNOWNHOST_KEY_ECDSA_521: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_521; + break; +#endif + +#ifdef LIBSSH2_HOSTKEY_TYPE_ED25519 + case LIBSSH2_HOSTKEY_TYPE_ED25519: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ED25519; + break; +#endif + default: + cert.raw_type = GIT_CERT_SSH_RAW_TYPE_UNKNOWN; + } + } + +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA256); + if (key != NULL) { + cert.type |= GIT_CERT_SSH_SHA256; + memcpy(&cert.hash_sha256, key, 32); + } +#endif + + key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1); + if (key != NULL) { + cert.type |= GIT_CERT_SSH_SHA1; + memcpy(&cert.hash_sha1, key, 20); + } + + key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_MD5); + if (key != NULL) { + cert.type |= GIT_CERT_SSH_MD5; + memcpy(&cert.hash_md5, key, 16); + } + + if (cert.type == 0) { + git_error_set(GIT_ERROR_SSH, "unable to get the host key"); + return -1; + } + + git_error_clear(); + error = 0; + if (!cert_valid) { + git_error_set(GIT_ERROR_SSH, "invalid or unknown remote ssh hostkey"); + error = GIT_ECERTIFICATE; + } + + if (check_cb != NULL) { + git_cert_hostkey *cert_ptr = &cert; + git_error_state previous_error = {0}; + + git_error_state_capture(&previous_error, error); + error = check_cb((git_cert *) cert_ptr, cert_valid, host, check_cb_payload); + if (error == GIT_PASSTHROUGH) { + error = git_error_state_restore(&previous_error); + } else if (error < 0 && !git_error_last()) { + git_error_set(GIT_ERROR_NET, "user canceled hostkey check"); + } + + git_error_state_free(&previous_error); + } + + return error; +} + #define SSH_DEFAULT_PORT "22" static int _git_ssh_setup_conn( @@ -493,93 +713,8 @@ static int _git_ssh_setup_conn( if ((error = _git_ssh_session_create(&session, s->io)) < 0) goto done; - if (t->owner->connect_opts.callbacks.certificate_check != NULL) { - git_cert_hostkey cert = {{ 0 }}, *cert_ptr; - const char *key; - size_t cert_len; - int cert_type; - - cert.parent.cert_type = GIT_CERT_HOSTKEY_LIBSSH2; - - key = libssh2_session_hostkey(session, &cert_len, &cert_type); - if (key != NULL) { - cert.type |= GIT_CERT_SSH_RAW; - cert.hostkey = key; - cert.hostkey_len = cert_len; - switch (cert_type) { - case LIBSSH2_HOSTKEY_TYPE_RSA: - cert.raw_type = GIT_CERT_SSH_RAW_TYPE_RSA; - break; - case LIBSSH2_HOSTKEY_TYPE_DSS: - cert.raw_type = GIT_CERT_SSH_RAW_TYPE_DSS; - break; - -#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_256 - case LIBSSH2_HOSTKEY_TYPE_ECDSA_256: - cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_256; - break; - case LIBSSH2_HOSTKEY_TYPE_ECDSA_384: - cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_384; - break; - case LIBSSH2_KNOWNHOST_KEY_ECDSA_521: - cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ECDSA_521; - break; -#endif - -#ifdef LIBSSH2_HOSTKEY_TYPE_ED25519 - case LIBSSH2_HOSTKEY_TYPE_ED25519: - cert.raw_type = GIT_CERT_SSH_RAW_TYPE_KEY_ED25519; - break; -#endif - default: - cert.raw_type = GIT_CERT_SSH_RAW_TYPE_UNKNOWN; - } - } - -#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 - key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA256); - if (key != NULL) { - cert.type |= GIT_CERT_SSH_SHA256; - memcpy(&cert.hash_sha256, key, 32); - } -#endif - - key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1); - if (key != NULL) { - cert.type |= GIT_CERT_SSH_SHA1; - memcpy(&cert.hash_sha1, key, 20); - } - - key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_MD5); - if (key != NULL) { - cert.type |= GIT_CERT_SSH_MD5; - memcpy(&cert.hash_md5, key, 16); - } - - if (cert.type == 0) { - git_error_set(GIT_ERROR_SSH, "unable to get the host key"); - error = -1; - goto done; - } - - /* We don't currently trust any hostkeys */ - git_error_clear(); - - cert_ptr = &cert; - - error = t->owner->connect_opts.callbacks.certificate_check( - (git_cert *)cert_ptr, - 0, - s->url.host, - t->owner->connect_opts.callbacks.payload); - - if (error < 0 && error != GIT_PASSTHROUGH) { - if (!git_error_last()) - git_error_set(GIT_ERROR_NET, "user cancelled hostkey check"); - - goto done; - } - } + if ((error = check_certificate(session, t->owner->connect_opts.callbacks.certificate_check, t->owner->connect_opts.callbacks.payload, s->url.host, s->url.port)) < 0) + goto done; /* we need the username to ask for auth methods */ if (!s->url.username) { From c29651fe8b8165b45a8cdf746f13e78694ff6f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 2 Nov 2022 10:58:14 +0100 Subject: [PATCH 2/4] tests: append the github.com ssh keys so we have access during tests Currently just the one test needs it. The ssh-rsa makes sure we're asking for the cipher we find in `known_hosts` as that won't be the one selected by default. This will be relevant in later changes. --- ci/test.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ci/test.sh b/ci/test.sh index 0e1d39e8d..230daaaa0 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -170,6 +170,11 @@ if [ -z "$SKIP_SSH_TESTS" ]; then echo "[localhost]:2222 $algorithm $key" >>"${HOME}/.ssh/known_hosts" done <"${SSHD_DIR}/id_rsa.pub" + # Append the github.com keys for the tests that don't override checks. + # We ask for ssh-rsa to test that the selection based off of known_hosts + # is working. + ssh-keyscan -t ssh-rsa github.com >>"${HOME}/.ssh/known_hosts" + # Get the fingerprint for localhost and remove the colons so we can # parse it as a hex number. Older versions have a different output # format. From a218f1bf74a3f35a6195a7f8da1a1af32b069633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 2 Nov 2022 11:42:19 +0100 Subject: [PATCH 3/4] tests: move online::clone::ssh_auth_methods into the ssh test suite We're currently running it as part of the online suite but that doesn't have any setup for ssh so we won't find the GitHub keys we set up during the test. It doesn't need the private key setup as we just want to make sure we see some auth request from the server, but with the addition of hostkey checking we're now seeing it fail when we skip these tests. --- tests/libgit2/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/libgit2/CMakeLists.txt b/tests/libgit2/CMakeLists.txt index 17f2253f0..f581d3075 100644 --- a/tests/libgit2/CMakeLists.txt +++ b/tests/libgit2/CMakeLists.txt @@ -66,11 +66,11 @@ endif() include(AddClarTest) add_clar_test(libgit2_tests offline -v -xonline) add_clar_test(libgit2_tests invasive -v -sfilter::stream::bigfile -sodb::largefiles -siterator::workdir::filesystem_gunk -srepo::init -srepo::init::at_filesystem_root) -add_clar_test(libgit2_tests online -v -sonline -xonline::customcert) +add_clar_test(libgit2_tests online -v -sonline -xonline::customcert -xonline::clone::ssh_auth_methods) add_clar_test(libgit2_tests online_customcert -v -sonline::customcert) add_clar_test(libgit2_tests gitdaemon -v -sonline::push) add_clar_test(libgit2_tests gitdaemon_namespace -v -sonline::clone::namespace) -add_clar_test(libgit2_tests ssh -v -sonline::push -sonline::clone::ssh_cert -sonline::clone::ssh_with_paths -sonline::clone::path_whitespace_ssh) +add_clar_test(libgit2_tests ssh -v -sonline::push -sonline::clone::ssh_cert -sonline::clone::ssh_with_paths -sonline::clone::path_whitespace_ssh -sonline::clone::ssh_auth_methods) add_clar_test(libgit2_tests proxy -v -sonline::clone::proxy) add_clar_test(libgit2_tests auth_clone -v -sonline::clone::cred) add_clar_test(libgit2_tests auth_clone_and_push -v -sonline::clone::push -sonline::push) From e33d7c06887582540bee013665f88d9625eca46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 2 Nov 2022 13:47:25 +0100 Subject: [PATCH 4/4] ssh: look for a key in known_hosts to set the key type for the handshake The server and client negotiate a single hostkey, but the "best" cipher may not be the one for which we have an entry in `known_hosts`. This can lead to us not finding the key in known_hosts even though we should be connecting. Instead here we look up the hostname with a nonsense key to perform a lookup in the known hosts and set that. This is roughly what the OpenSSH client does as well. --- src/libgit2/transports/ssh.c | 206 ++++++++++++++++++++++++++--------- 1 file changed, 153 insertions(+), 53 deletions(-) diff --git a/src/libgit2/transports/ssh.c b/src/libgit2/transports/ssh.c index 185d53959..85e779744 100644 --- a/src/libgit2/transports/ssh.c +++ b/src/libgit2/transports/ssh.c @@ -421,15 +421,119 @@ static int request_creds(git_credential **out, ssh_subtransport *t, const char * return 0; } +#define KNOWN_HOSTS_FILE ".ssh/known_hosts" + +/* + * Load the known_hosts file. + * + * Returns success but leaves the output NULL if we couldn't find the file. + */ +static int load_known_hosts(LIBSSH2_KNOWNHOSTS **hosts, LIBSSH2_SESSION *session) +{ + git_str path = GIT_STR_INIT, home = GIT_STR_INIT; + LIBSSH2_KNOWNHOSTS *known_hosts = NULL; + int error; + + GIT_ASSERT_ARG(hosts); + + if ((error = git__getenv(&home, "HOME")) < 0) + return error; + + if ((error = git_str_joinpath(&path, git_str_cstr(&home), KNOWN_HOSTS_FILE)) < 0) + goto out; + + if ((known_hosts = libssh2_knownhost_init(session)) == NULL) { + ssh_error(session, "error initializing known hosts"); + error = -1; + goto out; + } + + /* + * Try to read the file and consider not finding it as not trusting the + * host rather than an error. + */ + error = libssh2_knownhost_readfile(known_hosts, git_str_cstr(&path), LIBSSH2_KNOWNHOST_FILE_OPENSSH); + if (error == LIBSSH2_ERROR_FILE) + error = 0; + if (error < 0) + ssh_error(session, "error reading known_hosts"); + +out: + *hosts = known_hosts; + + git_str_clear(&home); + git_str_clear(&path); + + return error; +} + +static const char *hostkey_type_to_string(int type) +{ + switch (type) { + case LIBSSH2_KNOWNHOST_KEY_SSHRSA: + return "ssh-rsa"; + case LIBSSH2_KNOWNHOST_KEY_SSHDSS: + return "ssh-dss"; +#ifdef LIBSSH2_KNOWNHOST_KEY_ECDSA_256 + case LIBSSH2_KNOWNHOST_KEY_ECDSA_256: + return "ecdsa-sha2-nistp256"; + case LIBSSH2_KNOWNHOST_KEY_ECDSA_384: + return "ecdsa-sha2-nistp384"; + case LIBSSH2_KNOWNHOST_KEY_ECDSA_521: + return "ecdsa-sha2-nistp521"; +#endif +#ifdef LIBSSH2_KNOWNHOST_KEY_ED25519 + case LIBSSH2_KNOWNHOST_KEY_ED25519: + return "ssh-ed25519"; +#endif + } + + return NULL; +} + +/* + * We figure out what kind of key we want to ask the remote for by trying to + * look it up with a nonsense key and using that mismatch to figure out what key + * we do have stored for the host. + * + * Returns the string to pass to libssh2_session_method_pref or NULL if we were + * unable to find anything or an error happened. + */ +static const char *find_hostkey_preference(LIBSSH2_KNOWNHOSTS *known_hosts, const char *hostname, int port) +{ + struct libssh2_knownhost *host = NULL; + /* Specify no key type so we don't filter on that */ + int type = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW; + const char key = '\0'; + int error; + + /* + * In case of mismatch, we can find the type of key from known_hosts in + * the returned host's information as it means that an entry was found + * but our nonsense key obviously didn't match. + */ + error = libssh2_knownhost_checkp(known_hosts, hostname, port, &key, 1, type, &host); + if (error == LIBSSH2_KNOWNHOST_CHECK_MISMATCH) + return hostkey_type_to_string(host->typemask & LIBSSH2_KNOWNHOST_KEY_MASK); + + return NULL; +} + static int _git_ssh_session_create( LIBSSH2_SESSION **session, + LIBSSH2_KNOWNHOSTS **hosts, + const char *hostname, + int port, git_stream *io) { int rc = 0; LIBSSH2_SESSION *s; + LIBSSH2_KNOWNHOSTS *known_hosts; git_socket_stream *socket = GIT_CONTAINER_OF(io, git_socket_stream, parent); + const char *keytype = NULL; GIT_ASSERT_ARG(session); + GIT_ASSERT_ARG(hosts); s = libssh2_session_init(); if (!s) { @@ -437,23 +541,46 @@ static int _git_ssh_session_create( return -1; } + if ((rc = load_known_hosts(&known_hosts, s)) < 0) { + ssh_error(s, "error loading known_hosts"); + libssh2_session_free(s); + return -1; + } + + if ((keytype = find_hostkey_preference(known_hosts, hostname, port)) != NULL) { + do { + rc = libssh2_session_method_pref(s, LIBSSH2_METHOD_HOSTKEY, keytype); + } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); + if (rc != LIBSSH2_ERROR_NONE) { + ssh_error(s, "failed to set hostkey preference"); + goto on_error; + } + } + + do { rc = libssh2_session_handshake(s, socket->s); } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); if (rc != LIBSSH2_ERROR_NONE) { ssh_error(s, "failed to start SSH session"); - libssh2_session_free(s); - return -1; + goto on_error; } libssh2_session_set_blocking(s, 1); *session = s; + *hosts = known_hosts; return 0; + +on_error: + libssh2_knownhost_free(known_hosts); + libssh2_session_free(s); + return -1; } + /* * Returns the typemask argument to pass to libssh2_knownhost_check{,p} based on * the type of key that libssh2_session_hostkey returns. @@ -491,8 +618,6 @@ static int fingerprint_type_mask(int keytype) return mask; } -#define KNOWN_HOSTS_FILE ".ssh/known_hosts" - /* * Check the host against the user's known_hosts file. * @@ -500,62 +625,28 @@ static int fingerprint_type_mask(int keytype) */ static int check_against_known_hosts( LIBSSH2_SESSION *session, + LIBSSH2_KNOWNHOSTS *known_hosts, const char *hostname, int port, const char *key, size_t key_len, int key_type) { - int error, check, typemask, ret = 0; - git_str path = GIT_STR_INIT, home = GIT_STR_INIT; - LIBSSH2_KNOWNHOSTS *known_hosts = NULL; + int check, typemask, ret = 0; struct libssh2_knownhost *host = NULL; - if ((error = git__getenv(&home, "HOME")) < 0) { - return error; - } - - if ((error = git_str_joinpath(&path, git_str_cstr(&home), KNOWN_HOSTS_FILE)) < 0) { - ret = error; - goto out; - } - - if ((known_hosts = libssh2_knownhost_init(session)) == NULL) { - ssh_error(session, "error initializing known hosts"); - ret = -1; - goto out; - } - - /* - * Try to read the file and consider not finding it as not trusting the - * host rather than an error. - */ - error = libssh2_knownhost_readfile(known_hosts, git_str_cstr(&path), LIBSSH2_KNOWNHOST_FILE_OPENSSH); - if (error == LIBSSH2_ERROR_FILE) { - ret = 0; - goto out; - } - if (error < 0) { - ssh_error(session, "error reading known_hosts"); - ret = -1; - goto out; - } + if (known_hosts == NULL) + return 0; typemask = fingerprint_type_mask(key_type); check = libssh2_knownhost_checkp(known_hosts, hostname, port, key, key_len, typemask, &host); if (check == LIBSSH2_KNOWNHOST_CHECK_FAILURE) { ssh_error(session, "error checking for known host"); - ret = -1; - goto out; + return -1; } ret = check == LIBSSH2_KNOWNHOST_CHECK_MATCH ? 1 : 0; -out: - libssh2_knownhost_free(known_hosts); - git_str_clear(&path); - git_str_clear(&home); - return ret; } @@ -567,26 +658,23 @@ out: */ static int check_certificate( LIBSSH2_SESSION *session, + LIBSSH2_KNOWNHOSTS *known_hosts, git_transport_certificate_check_cb check_cb, void *check_cb_payload, const char *host, - const char *portstr) + int port) { git_cert_hostkey cert = {{ 0 }}; const char *key; size_t cert_len; - int cert_type, port, cert_valid = 0, error = 0; + int cert_type, cert_valid = 0, error = 0; if ((key = libssh2_session_hostkey(session, &cert_len, &cert_type)) == NULL) { ssh_error(session, "failed to retrieve hostkey"); return -1; } - /* Try to parse the port as a number, if we can't then fall back to default */ - if (git__strntol32(&port, portstr, strlen(portstr), NULL, 10) < 0) - port = -1; - - if ((cert_valid = check_against_known_hosts(session, host, port, key, cert_len, cert_type)) < 0) + if ((cert_valid = check_against_known_hosts(session, known_hosts, host, port, key, cert_len, cert_type)) < 0) return -1; cert.parent.cert_type = GIT_CERT_HOSTKEY_LIBSSH2; @@ -682,11 +770,12 @@ static int _git_ssh_setup_conn( const char *cmd, git_smart_subtransport_stream **stream) { - int auth_methods, error = 0; + int auth_methods, error = 0, port; ssh_stream *s; git_credential *cred = NULL; LIBSSH2_SESSION *session=NULL; LIBSSH2_CHANNEL *channel=NULL; + LIBSSH2_KNOWNHOSTS *known_hosts = NULL; t->current_stream = NULL; @@ -710,10 +799,19 @@ static int _git_ssh_setup_conn( (error = git_stream_connect(s->io)) < 0) goto done; - if ((error = _git_ssh_session_create(&session, s->io)) < 0) + /* + * Try to parse the port as a number, if we can't then fall back to + * default. It would be nice if we could get the port that was resolved + * as part of the stream connection, but that's not something that's + * exposed. + */ + if (git__strntol32(&port, s->url.port, strlen(s->url.port), NULL, 10) < 0) + port = -1; + + if ((error = _git_ssh_session_create(&session, &known_hosts, s->url.host, port, s->io)) < 0) goto done; - if ((error = check_certificate(session, t->owner->connect_opts.callbacks.certificate_check, t->owner->connect_opts.callbacks.payload, s->url.host, s->url.port)) < 0) + if ((error = check_certificate(session, known_hosts, t->owner->connect_opts.callbacks.certificate_check, t->owner->connect_opts.callbacks.payload, s->url.host, port)) < 0) goto done; /* we need the username to ask for auth methods */ @@ -786,6 +884,8 @@ done: if (error < 0) { ssh_stream_free(*stream); + if (known_hosts) + libssh2_knownhost_free(known_hosts); if (session) libssh2_session_free(session); }