Unverified Commit 42e5db98 by Edward Thomson Committed by GitHub

Merge pull request #6450 from libgit2/1.5_ssh

ssh: perform host key checking by default
parents fbea439d 1fda9492
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
cmake_minimum_required(VERSION 3.5.1) 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 # Add find modules to the path
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake") set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake")
......
...@@ -144,6 +144,11 @@ if [ -z "$SKIP_SSH_TESTS" ]; then ...@@ -144,6 +144,11 @@ if [ -z "$SKIP_SSH_TESTS" ]; then
echo "[localhost]:2222 $algorithm $key" >>"${HOME}/.ssh/known_hosts" echo "[localhost]:2222 $algorithm $key" >>"${HOME}/.ssh/known_hosts"
done <"${SSHD_DIR}/id_rsa.pub" 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 # Get the fingerprint for localhost and remove the colons so we can
# parse it as a hex number. Older versions have a different output # parse it as a hex number. Older versions have a different output
# format. # format.
......
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 v1.5
---- ----
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
* The version string for libgit2. This string follows semantic * The version string for libgit2. This string follows semantic
* versioning (v2) guidelines. * versioning (v2) guidelines.
*/ */
#define LIBGIT2_VERSION "1.5.0" #define LIBGIT2_VERSION "1.5.1"
/** The major version number for this version of libgit2. */ /** The major version number for this version of libgit2. */
#define LIBGIT2_VER_MAJOR 1 #define LIBGIT2_VER_MAJOR 1
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
#define LIBGIT2_VER_MINOR 5 #define LIBGIT2_VER_MINOR 5
/** The revision ("teeny") version number for this version of libgit2. */ /** 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. */ /** The Windows DLL patch number for this version of libgit2. */
#define LIBGIT2_VER_PATCH 0 #define LIBGIT2_VER_PATCH 0
......
{ {
"name": "libgit2", "name": "libgit2",
"version": "1.5.0", "version": "1.5.1",
"repo": "https://github.com/libgit2/libgit2", "repo": "https://github.com/libgit2/libgit2",
"description": " A cross-platform, linkable library implementation of Git that you can use in your application.", "description": " A cross-platform, linkable library implementation of Git that you can use in your application.",
"install": "mkdir build && cd build && cmake .. && cmake --build ." "install": "mkdir build && cd build && cmake .. && cmake --build ."
......
...@@ -421,15 +421,119 @@ static int request_creds(git_credential **out, ssh_subtransport *t, const char * ...@@ -421,15 +421,119 @@ static int request_creds(git_credential **out, ssh_subtransport *t, const char *
return 0; 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( static int _git_ssh_session_create(
LIBSSH2_SESSION **session, LIBSSH2_SESSION **session,
LIBSSH2_KNOWNHOSTS **hosts,
const char *hostname,
int port,
git_stream *io) git_stream *io)
{ {
int rc = 0; int rc = 0;
LIBSSH2_SESSION *s; LIBSSH2_SESSION *s;
LIBSSH2_KNOWNHOSTS *known_hosts;
git_socket_stream *socket = GIT_CONTAINER_OF(io, git_socket_stream, parent); git_socket_stream *socket = GIT_CONTAINER_OF(io, git_socket_stream, parent);
const char *keytype = NULL;
GIT_ASSERT_ARG(session); GIT_ASSERT_ARG(session);
GIT_ASSERT_ARG(hosts);
s = libssh2_session_init(); s = libssh2_session_init();
if (!s) { if (!s) {
...@@ -437,71 +541,143 @@ static int _git_ssh_session_create( ...@@ -437,71 +541,143 @@ static int _git_ssh_session_create(
return -1; 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 { do {
rc = libssh2_session_handshake(s, socket->s); rc = libssh2_session_handshake(s, socket->s);
} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc); } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
if (rc != LIBSSH2_ERROR_NONE) { if (rc != LIBSSH2_ERROR_NONE) {
ssh_error(s, "failed to start SSH session"); ssh_error(s, "failed to start SSH session");
libssh2_session_free(s); goto on_error;
return -1;
} }
libssh2_session_set_blocking(s, 1); libssh2_session_set_blocking(s, 1);
*session = s; *session = s;
*hosts = known_hosts;
return 0; return 0;
on_error:
libssh2_knownhost_free(known_hosts);
libssh2_session_free(s);
return -1;
} }
#define SSH_DEFAULT_PORT "22"
static int _git_ssh_setup_conn( /*
ssh_subtransport *t, * Returns the typemask argument to pass to libssh2_knownhost_check{,p} based on
const char *url, * the type of key that libssh2_session_hostkey returns.
const char *cmd, */
git_smart_subtransport_stream **stream) static int fingerprint_type_mask(int keytype)
{ {
int auth_methods, error = 0; int mask = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW;
ssh_stream *s; return mask;
git_credential *cred = NULL;
LIBSSH2_SESSION *session=NULL;
LIBSSH2_CHANNEL *channel=NULL;
t->current_stream = NULL; 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
}
*stream = NULL; return mask;
if (ssh_stream_alloc(t, cmd, stream) < 0) }
return -1;
s = (ssh_stream *)*stream; /*
s->session = NULL; * Check the host against the user's known_hosts file.
s->channel = NULL; *
* Returns 1/0 for valid/''not-valid or <0 for an error
*/
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 check, typemask, ret = 0;
struct libssh2_knownhost *host = NULL;
if (git_net_str_is_url(url)) if (known_hosts == NULL)
error = git_net_url_parse(&s->url, url); return 0;
else
error = git_net_url_parse_scp(&s->url, url);
if (error < 0) typemask = fingerprint_type_mask(key_type);
goto done; 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");
return -1;
}
if ((error = git_socket_stream_new(&s->io, s->url.host, s->url.port)) < 0 || ret = check == LIBSSH2_KNOWNHOST_CHECK_MATCH ? 1 : 0;
(error = git_stream_connect(s->io)) < 0)
goto done;
if ((error = _git_ssh_session_create(&session, s->io)) < 0) return ret;
goto done; }
if (t->owner->connect_opts.callbacks.certificate_check != NULL) { /*
git_cert_hostkey cert = {{ 0 }}, *cert_ptr; * 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,
LIBSSH2_KNOWNHOSTS *known_hosts,
git_transport_certificate_check_cb check_cb,
void *check_cb_payload,
const char *host,
int port)
{
git_cert_hostkey cert = {{ 0 }};
const char *key; const char *key;
size_t cert_len; size_t cert_len;
int cert_type; int cert_type, cert_valid = 0, error = 0;
cert.parent.cert_type = GIT_CERT_HOSTKEY_LIBSSH2; if ((key = libssh2_session_hostkey(session, &cert_len, &cert_type)) == NULL) {
ssh_error(session, "failed to retrieve hostkey");
return -1;
}
if ((cert_valid = check_against_known_hosts(session, known_hosts, host, port, key, cert_len, cert_type)) < 0)
return -1;
key = libssh2_session_hostkey(session, &cert_len, &cert_type); cert.parent.cert_type = GIT_CERT_HOSTKEY_LIBSSH2;
if (key != NULL) { if (key != NULL) {
cert.type |= GIT_CERT_SSH_RAW; cert.type |= GIT_CERT_SSH_RAW;
cert.hostkey = key; cert.hostkey = key;
...@@ -558,28 +734,85 @@ static int _git_ssh_setup_conn( ...@@ -558,28 +734,85 @@ static int _git_ssh_setup_conn(
if (cert.type == 0) { if (cert.type == 0) {
git_error_set(GIT_ERROR_SSH, "unable to get the host key"); git_error_set(GIT_ERROR_SSH, "unable to get the host key");
error = -1; return -1;
goto done;
} }
/* We don't currently trust any hostkeys */
git_error_clear(); git_error_clear();
error = 0;
if (!cert_valid) {
git_error_set(GIT_ERROR_SSH, "invalid or unknown remote ssh hostkey");
error = GIT_ECERTIFICATE;
}
cert_ptr = &cert; if (check_cb != NULL) {
git_cert_hostkey *cert_ptr = &cert;
git_error_state previous_error = {0};
error = t->owner->connect_opts.callbacks.certificate_check( git_error_state_capture(&previous_error, error);
(git_cert *)cert_ptr, error = check_cb((git_cert *) cert_ptr, cert_valid, host, check_cb_payload);
0, if (error == GIT_PASSTHROUGH) {
s->url.host, error = git_error_state_restore(&previous_error);
t->owner->connect_opts.callbacks.payload); } else if (error < 0 && !git_error_last()) {
git_error_set(GIT_ERROR_NET, "user canceled hostkey check");
}
if (error < 0 && error != GIT_PASSTHROUGH) { git_error_state_free(&previous_error);
if (!git_error_last()) }
git_error_set(GIT_ERROR_NET, "user cancelled hostkey check");
return error;
}
#define SSH_DEFAULT_PORT "22"
static int _git_ssh_setup_conn(
ssh_subtransport *t,
const char *url,
const char *cmd,
git_smart_subtransport_stream **stream)
{
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;
*stream = NULL;
if (ssh_stream_alloc(t, cmd, stream) < 0)
return -1;
s = (ssh_stream *)*stream;
s->session = NULL;
s->channel = NULL;
if (git_net_str_is_url(url))
error = git_net_url_parse(&s->url, url);
else
error = git_net_url_parse_scp(&s->url, url);
if (error < 0)
goto done;
if ((error = git_socket_stream_new(&s->io, s->url.host, s->url.port)) < 0 ||
(error = git_stream_connect(s->io)) < 0)
goto done;
/*
* 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, known_hosts, t->owner->connect_opts.callbacks.certificate_check, t->owner->connect_opts.callbacks.payload, s->url.host, port)) < 0)
goto done; goto done;
}
}
/* we need the username to ask for auth methods */ /* we need the username to ask for auth methods */
if (!s->url.username) { if (!s->url.username) {
...@@ -651,6 +884,8 @@ done: ...@@ -651,6 +884,8 @@ done:
if (error < 0) { if (error < 0) {
ssh_stream_free(*stream); ssh_stream_free(*stream);
if (known_hosts)
libssh2_knownhost_free(known_hosts);
if (session) if (session)
libssh2_session_free(session); libssh2_session_free(session);
} }
......
...@@ -66,10 +66,10 @@ endif() ...@@ -66,10 +66,10 @@ endif()
include(AddClarTest) include(AddClarTest)
add_clar_test(libgit2_tests offline -v -xonline) 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 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 online_customcert -v -sonline::customcert)
add_clar_test(libgit2_tests gitdaemon -v -sonline::push) 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 proxy -v -sonline::clone::proxy)
add_clar_test(libgit2_tests auth_clone -v -sonline::clone::cred) 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) add_clar_test(libgit2_tests auth_clone_and_push -v -sonline::clone::push -sonline::push)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment