@@ -421,39 +421,166 @@ static int request_creds(git_credential **out, ssh_subtransport *t, const char *
421421 return 0 ;
422422}
423423
424+ #define KNOWN_HOSTS_FILE ".ssh/known_hosts"
425+
426+ /*
427+ * Load the known_hosts file.
428+ *
429+ * Returns success but leaves the output NULL if we couldn't find the file.
430+ */
431+ static int load_known_hosts (LIBSSH2_KNOWNHOSTS * * hosts , LIBSSH2_SESSION * session )
432+ {
433+ git_str path = GIT_STR_INIT , home = GIT_STR_INIT ;
434+ LIBSSH2_KNOWNHOSTS * known_hosts = NULL ;
435+ int error ;
436+
437+ GIT_ASSERT_ARG (hosts );
438+
439+ if ((error = git__getenv (& home , "HOME" )) < 0 )
440+ return error ;
441+
442+ if ((error = git_str_joinpath (& path , git_str_cstr (& home ), KNOWN_HOSTS_FILE )) < 0 )
443+ goto out ;
444+
445+ if ((known_hosts = libssh2_knownhost_init (session )) == NULL ) {
446+ ssh_error (session , "error initializing known hosts" );
447+ error = -1 ;
448+ goto out ;
449+ }
450+
451+ /*
452+ * Try to read the file and consider not finding it as not trusting the
453+ * host rather than an error.
454+ */
455+ error = libssh2_knownhost_readfile (known_hosts , git_str_cstr (& path ), LIBSSH2_KNOWNHOST_FILE_OPENSSH );
456+ if (error == LIBSSH2_ERROR_FILE )
457+ error = 0 ;
458+ if (error < 0 )
459+ ssh_error (session , "error reading known_hosts" );
460+
461+ out :
462+ * hosts = known_hosts ;
463+
464+ git_str_clear (& home );
465+ git_str_clear (& path );
466+
467+ return error ;
468+ }
469+
470+ static const char * hostkey_type_to_string (int type )
471+ {
472+ switch (type ) {
473+ case LIBSSH2_KNOWNHOST_KEY_SSHRSA :
474+ return "ssh-rsa" ;
475+ case LIBSSH2_KNOWNHOST_KEY_SSHDSS :
476+ return "ssh-dss" ;
477+ #ifdef LIBSSH2_KNOWNHOST_KEY_ECDSA_256
478+ case LIBSSH2_KNOWNHOST_KEY_ECDSA_256 :
479+ return "ecdsa-sha2-nistp256" ;
480+ case LIBSSH2_KNOWNHOST_KEY_ECDSA_384 :
481+ return "ecdsa-sha2-nistp384" ;
482+ case LIBSSH2_KNOWNHOST_KEY_ECDSA_521 :
483+ return "ecdsa-sha2-nistp521" ;
484+ #endif
485+ #ifdef LIBSSH2_KNOWNHOST_KEY_ED25519
486+ case LIBSSH2_KNOWNHOST_KEY_ED25519 :
487+ return "ssh-ed25519" ;
488+ #endif
489+ }
490+
491+ return NULL ;
492+ }
493+
494+ /*
495+ * We figure out what kind of key we want to ask the remote for by trying to
496+ * look it up with a nonsense key and using that mismatch to figure out what key
497+ * we do have stored for the host.
498+ *
499+ * Returns the string to pass to libssh2_session_method_pref or NULL if we were
500+ * unable to find anything or an error happened.
501+ */
502+ static const char * find_hostkey_preference (LIBSSH2_KNOWNHOSTS * known_hosts , const char * hostname , int port )
503+ {
504+ struct libssh2_knownhost * host = NULL ;
505+ /* Specify no key type so we don't filter on that */
506+ int type = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW ;
507+ const char key = '\0' ;
508+ int error ;
509+
510+ /*
511+ * In case of mismatch, we can find the type of key from known_hosts in
512+ * the returned host's information as it means that an entry was found
513+ * but our nonsense key obviously didn't match.
514+ */
515+ error = libssh2_knownhost_checkp (known_hosts , hostname , port , & key , 1 , type , & host );
516+ if (error == LIBSSH2_KNOWNHOST_CHECK_MISMATCH )
517+ return hostkey_type_to_string (host -> typemask & LIBSSH2_KNOWNHOST_KEY_MASK );
518+
519+ return NULL ;
520+ }
521+
424522static int _git_ssh_session_create (
425523 LIBSSH2_SESSION * * session ,
524+ LIBSSH2_KNOWNHOSTS * * hosts ,
525+ const char * hostname ,
526+ int port ,
426527 git_stream * io )
427528{
428529 int rc = 0 ;
429530 LIBSSH2_SESSION * s ;
531+ LIBSSH2_KNOWNHOSTS * known_hosts ;
430532 git_socket_stream * socket = GIT_CONTAINER_OF (io , git_socket_stream , parent );
533+ const char * keytype = NULL ;
431534
432535 GIT_ASSERT_ARG (session );
536+ GIT_ASSERT_ARG (hosts );
433537
434538 s = libssh2_session_init ();
435539 if (!s ) {
436540 git_error_set (GIT_ERROR_NET , "failed to initialize SSH session" );
437541 return -1 ;
438542 }
439543
544+ if ((rc = load_known_hosts (& known_hosts , s )) < 0 ) {
545+ ssh_error (s , "error loading known_hosts" );
546+ libssh2_session_free (s );
547+ return -1 ;
548+ }
549+
550+ if ((keytype = find_hostkey_preference (known_hosts , hostname , port )) != NULL ) {
551+ do {
552+ rc = libssh2_session_method_pref (s , LIBSSH2_METHOD_HOSTKEY , keytype );
553+ } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc );
554+ if (rc != LIBSSH2_ERROR_NONE ) {
555+ ssh_error (s , "failed to set hostkey preference" );
556+ goto on_error ;
557+ }
558+ }
559+
560+
440561 do {
441562 rc = libssh2_session_handshake (s , socket -> s );
442563 } while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc );
443564
444565 if (rc != LIBSSH2_ERROR_NONE ) {
445566 ssh_error (s , "failed to start SSH session" );
446- libssh2_session_free (s );
447- return -1 ;
567+ goto on_error ;
448568 }
449569
450570 libssh2_session_set_blocking (s , 1 );
451571
452572 * session = s ;
573+ * hosts = known_hosts ;
453574
454575 return 0 ;
576+
577+ on_error :
578+ libssh2_knownhost_free (known_hosts );
579+ libssh2_session_free (s );
580+ return -1 ;
455581}
456582
583+
457584/*
458585 * Returns the typemask argument to pass to libssh2_knownhost_check{,p} based on
459586 * the type of key that libssh2_session_hostkey returns.
@@ -491,71 +618,35 @@ static int fingerprint_type_mask(int keytype)
491618 return mask ;
492619}
493620
494- #define KNOWN_HOSTS_FILE ".ssh/known_hosts"
495-
496621/*
497622 * Check the host against the user's known_hosts file.
498623 *
499624 * Returns 1/0 for valid/''not-valid or <0 for an error
500625 */
501626static int check_against_known_hosts (
502627 LIBSSH2_SESSION * session ,
628+ LIBSSH2_KNOWNHOSTS * known_hosts ,
503629 const char * hostname ,
504630 int port ,
505631 const char * key ,
506632 size_t key_len ,
507633 int key_type )
508634{
509- int error , check , typemask , ret = 0 ;
510- git_str path = GIT_STR_INIT , home = GIT_STR_INIT ;
511- LIBSSH2_KNOWNHOSTS * known_hosts = NULL ;
635+ int check , typemask , ret = 0 ;
512636 struct libssh2_knownhost * host = NULL ;
513637
514- if ((error = git__getenv (& home , "HOME" )) < 0 ) {
515- return error ;
516- }
517-
518- if ((error = git_str_joinpath (& path , git_str_cstr (& home ), KNOWN_HOSTS_FILE )) < 0 ) {
519- ret = error ;
520- goto out ;
521- }
522-
523- if ((known_hosts = libssh2_knownhost_init (session )) == NULL ) {
524- ssh_error (session , "error initializing known hosts" );
525- ret = -1 ;
526- goto out ;
527- }
528-
529- /*
530- * Try to read the file and consider not finding it as not trusting the
531- * host rather than an error.
532- */
533- error = libssh2_knownhost_readfile (known_hosts , git_str_cstr (& path ), LIBSSH2_KNOWNHOST_FILE_OPENSSH );
534- if (error == LIBSSH2_ERROR_FILE ) {
535- ret = 0 ;
536- goto out ;
537- }
538- if (error < 0 ) {
539- ssh_error (session , "error reading known_hosts" );
540- ret = -1 ;
541- goto out ;
542- }
638+ if (known_hosts == NULL )
639+ return 0 ;
543640
544641 typemask = fingerprint_type_mask (key_type );
545642 check = libssh2_knownhost_checkp (known_hosts , hostname , port , key , key_len , typemask , & host );
546643 if (check == LIBSSH2_KNOWNHOST_CHECK_FAILURE ) {
547644 ssh_error (session , "error checking for known host" );
548- ret = -1 ;
549- goto out ;
645+ return -1 ;
550646 }
551647
552648 ret = check == LIBSSH2_KNOWNHOST_CHECK_MATCH ? 1 : 0 ;
553649
554- out :
555- libssh2_knownhost_free (known_hosts );
556- git_str_clear (& path );
557- git_str_clear (& home );
558-
559650 return ret ;
560651}
561652
@@ -567,26 +658,23 @@ static int check_against_known_hosts(
567658 */
568659static int check_certificate (
569660 LIBSSH2_SESSION * session ,
661+ LIBSSH2_KNOWNHOSTS * known_hosts ,
570662 git_transport_certificate_check_cb check_cb ,
571663 void * check_cb_payload ,
572664 const char * host ,
573- const char * portstr )
665+ int port )
574666{
575667 git_cert_hostkey cert = {{ 0 }};
576668 const char * key ;
577669 size_t cert_len ;
578- int cert_type , port , cert_valid = 0 , error = 0 ;
670+ int cert_type , cert_valid = 0 , error = 0 ;
579671
580672 if ((key = libssh2_session_hostkey (session , & cert_len , & cert_type )) == NULL ) {
581673 ssh_error (session , "failed to retrieve hostkey" );
582674 return -1 ;
583675 }
584676
585- /* Try to parse the port as a number, if we can't then fall back to default */
586- if (git__strntol32 (& port , portstr , strlen (portstr ), NULL , 10 ) < 0 )
587- port = -1 ;
588-
589- if ((cert_valid = check_against_known_hosts (session , host , port , key , cert_len , cert_type )) < 0 )
677+ if ((cert_valid = check_against_known_hosts (session , known_hosts , host , port , key , cert_len , cert_type )) < 0 )
590678 return -1 ;
591679
592680 cert .parent .cert_type = GIT_CERT_HOSTKEY_LIBSSH2 ;
@@ -682,11 +770,12 @@ static int _git_ssh_setup_conn(
682770 const char * cmd ,
683771 git_smart_subtransport_stream * * stream )
684772{
685- int auth_methods , error = 0 ;
773+ int auth_methods , error = 0 , port ;
686774 ssh_stream * s ;
687775 git_credential * cred = NULL ;
688776 LIBSSH2_SESSION * session = NULL ;
689777 LIBSSH2_CHANNEL * channel = NULL ;
778+ LIBSSH2_KNOWNHOSTS * known_hosts = NULL ;
690779
691780 t -> current_stream = NULL ;
692781
@@ -710,10 +799,19 @@ static int _git_ssh_setup_conn(
710799 (error = git_stream_connect (s -> io )) < 0 )
711800 goto done ;
712801
713- if ((error = _git_ssh_session_create (& session , s -> io )) < 0 )
802+ /*
803+ * Try to parse the port as a number, if we can't then fall back to
804+ * default. It would be nice if we could get the port that was resolved
805+ * as part of the stream connection, but that's not something that's
806+ * exposed.
807+ */
808+ if (git__strntol32 (& port , s -> url .port , strlen (s -> url .port ), NULL , 10 ) < 0 )
809+ port = -1 ;
810+
811+ if ((error = _git_ssh_session_create (& session , & known_hosts , s -> url .host , port , s -> io )) < 0 )
714812 goto done ;
715813
716- 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 )
814+ 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 )
717815 goto done ;
718816
719817 /* we need the username to ask for auth methods */
@@ -786,6 +884,8 @@ static int _git_ssh_setup_conn(
786884 if (error < 0 ) {
787885 ssh_stream_free (* stream );
788886
887+ if (known_hosts )
888+ libssh2_knownhost_free (known_hosts );
789889 if (session )
790890 libssh2_session_free (session );
791891 }
0 commit comments