From 6d63afcee71ef943a1fcd69c24f0562e9c8e429a 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: 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) { -- cgit v1.2.1 From 6e5b1498d08a46ba40b86ea822eddfcea7062fba 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: 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 0815522a9..60d94caf8 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -144,6 +144,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. -- cgit v1.2.1 From 52ebc04774e42569834c3faa5395ce3259110eb2 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: 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 27f421ad6..280b9a8ef 100644 --- a/tests/libgit2/CMakeLists.txt +++ b/tests/libgit2/CMakeLists.txt @@ -66,10 +66,10 @@ endif() include(AddClarTest) add_clar_test(libgit2_tests offline -v -xonline) add_clar_test(libgit2_tests invasive -v -score::ftruncate -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 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) -- cgit v1.2.1 From 63212a929d939ecff6e0358bd69a4a14bc96f855 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: 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); } -- cgit v1.2.1 From 6a4421ff333d8c21962b387c0457354f157193f1 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 18 Jan 2023 23:00:47 +0000 Subject: meta: update version numbers to 1.5.1 --- CMakeLists.txt | 2 +- include/git2/version.h | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b5a2842c..e76fc129a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.5.1) -project(libgit2 VERSION "1.5.0" LANGUAGES C) +project(libgit2 VERSION "1.5.1" LANGUAGES C) # Add find modules to the path set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake") diff --git a/include/git2/version.h b/include/git2/version.h index d591904fd..944d74365 100644 --- a/include/git2/version.h +++ b/include/git2/version.h @@ -11,7 +11,7 @@ * The version string for libgit2. This string follows semantic * versioning (v2) guidelines. */ -#define LIBGIT2_VERSION "1.5.0" +#define LIBGIT2_VERSION "1.5.1" /** The major version number for this version of libgit2. */ #define LIBGIT2_VER_MAJOR 1 @@ -20,7 +20,7 @@ #define LIBGIT2_VER_MINOR 5 /** The revision ("teeny") version number for this version of libgit2. */ -#define LIBGIT2_VER_REVISION 0 +#define LIBGIT2_VER_REVISION 1 /** The Windows DLL patch number for this version of libgit2. */ #define LIBGIT2_VER_PATCH 0 diff --git a/package.json b/package.json index 8676fe513..8e8176f22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "libgit2", - "version": "1.5.0", + "version": "1.5.1", "repo": "https://github.com/libgit2/libgit2", "description": " A cross-platform, linkable library implementation of Git that you can use in your application.", "install": "mkdir build && cd build && cmake .. && cmake --build ." -- cgit v1.2.1 From 1fda949267a4416cfaa5938a017c80162164f894 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 18 Jan 2023 23:02:12 +0000 Subject: meta: add changelog for v1.5.1 --- docs/changelog.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 989800a11..448a8a0ff 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,14 @@ +v1.5.1 +------ + +🔒 This is a security release to address CVE-2023-22742: when compiled using the optional, included libssh2 backend, libgit2 fails to verify SSH keys by default. + +When using an SSH remote with the optional, included libssh2 backend, libgit2 does not perform certificate checking by default. Prior versions of libgit2 require the caller to set the `certificate_check` field of libgit2's `git_remote_callbacks` structure - if a certificate check callback is not set, libgit2 does not perform any certificate checking. This means that by default - without configuring a certificate check callback, clients will not perform validation on the server SSH keys and may be subject to a man-in-the-middle attack. + +The libgit2 security team would like to thank the Julia and Rust security teams for responsibly disclosing this vulnerability and assisting with fixing the vulnerability. + +All users of the v1.5 release line are recommended to upgrade. + v1.5 ---- -- cgit v1.2.1