]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
libssh: add support for SHA256 host public keys
authorViktor Szakats <commit@vsz.me>
Thu, 14 May 2026 11:32:46 +0000 (13:32 +0200)
committerViktor Szakats <commit@vsz.me>
Fri, 15 May 2026 09:49:06 +0000 (11:49 +0200)
Reported-by: Joshua Rogers
Fixes #21605

Closes #21607

.github/workflows/windows.yml
docs/cmdline-opts/hostpubsha256.md
docs/libcurl/opts/CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256.md
lib/setopt.c
lib/vssh/libssh.c
lib/vssh/libssh2.c
src/tool_getparam.c
src/tool_libinfo.c
src/tool_libinfo.h
tests/data/test3021
tests/data/test3022

index 696fd09bab9ae781976f7c7b40827bb65085e1d9..67db3798fef2918188c1d686ae665b6c928cdee2 100644 (file)
@@ -522,11 +522,9 @@ jobs:
             fi
           fi
           if [ -n "${MATRIX_OPENSSH}" ]; then  # OpenSSH-Windows
-            TFLAGS+=' ~601 ~603 ~617 ~619 ~621 ~641 ~665 ~2004'  # SCP
+            TFLAGS+=' ~601 ~603 ~617 ~619 ~621 ~641 ~665 ~2004 ~3022'  # SCP
             if [[ "${MATRIX_INSTALL} " = *'libssh '* ]]; then
               TFLAGS+=' ~614'  # 'SFTP pre-quote chmod' SFTP, pre-quote, directory
-            else
-              TFLAGS+=' ~3022'  # 'SCP correct sha256 host key' SCP, server sha256 key check
             fi
           fi
           if [ "${MATRIX_OPENSSH}" = 'OpenSSH-Windows' ]; then
index e695a10cb58858db3f5b8726cbac993a5b4cea24..a92dbe5d7cb91efa3c7a2cd63a3f32b68305a697 100644 (file)
@@ -18,6 +18,3 @@ Example:
 
 Pass a string containing a Base64-encoded SHA256 hash of the remote host's
 public key. curl refuses the connection with the host unless the hashes match.
-
-This feature requires libcurl to be built with libssh2 and does not work with
-other SSH backends.
index 43a6d9e708e5734b9832f0d764316a5dafeda0ba..fce7e58f04bf355ebb0d6ef82ab5cf49eb318d3a 100644 (file)
@@ -73,10 +73,6 @@ int main(void)
 }
 ~~~
 
-# NOTES
-
-Requires the libssh2 backend.
-
 # %AVAILABILITY%
 
 # RETURN VALUE
index 61d87be06fcfc83b866e3968caa36d744cd3f594..0fc5ec7e87faecb0de53b9dd090e316a40739c3a 100644 (file)
@@ -2342,6 +2342,12 @@ static CURLcode setopt_cptr(struct Curl_easy *data, CURLoption option,
      * for validation purposes.
      */
     return Curl_setstropt(&s->str[STRING_SSH_HOST_PUBLIC_KEY_MD5], ptr);
+  case CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256:
+    /*
+     * Option to allow for the SHA256 of the host public key to be checked
+     * for validation purposes.
+     */
+    return Curl_setstropt(&s->str[STRING_SSH_HOST_PUBLIC_KEY_SHA256], ptr);
   case CURLOPT_SSH_KNOWNHOSTS:
     /*
      * Store the filename to read known hosts from.
@@ -2349,12 +2355,6 @@ static CURLcode setopt_cptr(struct Curl_easy *data, CURLoption option,
     return Curl_setstropt(&s->str[STRING_SSH_KNOWNHOSTS], ptr);
 #endif
 #ifdef USE_LIBSSH2
-  case CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256:
-    /*
-     * Option to allow for the SHA256 of the host public key to be checked
-     * for validation purposes.
-     */
-    return Curl_setstropt(&s->str[STRING_SSH_HOST_PUBLIC_KEY_SHA256], ptr);
   case CURLOPT_SSH_HOSTKEYDATA:
     /*
      * Custom client data to pass to the SSH keyfunc callback
index 49f9d3f93d2a4bf81c212c4b8b50bcc1e71580b7..d37dcd712fa71d82a8dec0be1a819358847d8cee 100644 (file)
@@ -57,6 +57,7 @@
 #include "multiif.h"
 #include "select.h"
 #include "vssh/vssh.h"
+#include "curlx/base64.h" /* for curlx_base64_encode() */
 
 #ifdef HAVE_UNISTD_H
 #include <unistd.h>
@@ -109,12 +110,14 @@ static CURLcode sftp_error_to_CURLE(int err)
 }
 
 /* Multiple options:
- * 1. data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5] is set with an MD5
+ * 1. data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256] is set with a SHA256
+ *    hash.
+ * 2. data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5] is set with an MD5
  *    hash (90s style auth, not sure we should have it here)
- * 2. data->set.ssh_keyfunc callback is set. Then we do trust on first
+ * 3. data->set.ssh_keyfunc callback is set. Then we do trust on first
  *    use. We even save on knownhosts if CURLKHSTAT_FINE_ADD_TO_FILE
  *    is returned by it.
- * 3. none of the above. We only accept if it is present on known hosts.
+ * 4. none of the above. We only accept if it is present on known hosts.
  *
  * Returns SSH_OK or SSH_ERROR.
  */
@@ -122,8 +125,10 @@ static int myssh_is_known(struct Curl_easy *data, struct ssh_conn *sshc)
 {
   int rc;
   ssh_key pubkey;
-  size_t hlen;
-  unsigned char *hash = NULL;
+  unsigned char *hash_sha256 = NULL;
+  size_t hlen_sha256;
+  unsigned char *hash_md5 = NULL;
+  size_t hlen_md5;
   char *found_base64 = NULL;
   char *known_base64 = NULL;
   int vstate;
@@ -139,20 +144,75 @@ static int myssh_is_known(struct Curl_easy *data, struct ssh_conn *sshc)
   if(rc != SSH_OK)
     return rc;
 
+  if(data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256]) {
+    const char *pubkey_sha256 =
+      data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256];
+    char *fingerprint_b64 = NULL;
+    size_t fingerprint_b64_len;
+    size_t pub_pos = 0;
+    size_t b64_pos = 0;
+
+    rc = ssh_get_publickey_hash(pubkey, SSH_PUBLICKEY_HASH_SHA256,
+                                &hash_sha256, &hlen_sha256);
+    if(rc != SSH_OK || hlen_sha256 != 32) {
+      failf(data, "Denied establishing ssh session: "
+            "SHA256 fingerprint not available");
+      goto cleanup;
+    }
+
+    if(curlx_base64_encode((const uint8_t *)hash_sha256, 32, &fingerprint_b64,
+                           &fingerprint_b64_len) != CURLE_OK) {
+      rc = SSH_ERROR;
+      goto cleanup;
+    }
+
+    infof(data, "SSH SHA256 fingerprint: %s", fingerprint_b64);
+
+    /* Find the position of any = padding characters in the public key */
+    while((pubkey_sha256[pub_pos] != '=') && pubkey_sha256[pub_pos]) {
+      pub_pos++;
+    }
+
+    /* Find the position of any = padding characters in the base64 coded
+     * hostkey fingerprint */
+    while((fingerprint_b64[b64_pos] != '=') && fingerprint_b64[b64_pos]) {
+      b64_pos++;
+    }
+
+    /* Before we authenticate we check the hostkey's SHA256 fingerprint
+     * against a known fingerprint, if available.
+     */
+    if((pub_pos != b64_pos) ||
+       strncmp(fingerprint_b64, pubkey_sha256, pub_pos)) {
+      failf(data,
+            "Denied establishing ssh session: mismatch SHA256 fingerprint. "
+            "Remote %s is not equal to %s", fingerprint_b64, pubkey_sha256);
+      curlx_free(fingerprint_b64);
+      rc = SSH_ERROR;
+      goto cleanup;
+    }
+
+    curlx_free(fingerprint_b64);
+
+    rc = SSH_OK;
+    goto cleanup;
+  }
+
   if(data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5]) {
-    int i;
-    char md5buffer[33];
     const char *pubkey_md5 = data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5];
+    char md5buffer[33];
+    int i;
 
-    rc = ssh_get_publickey_hash(pubkey, SSH_PUBLICKEY_HASH_MD5, &hash, &hlen);
-    if(rc != SSH_OK || hlen != 16) {
+    rc = ssh_get_publickey_hash(pubkey, SSH_PUBLICKEY_HASH_MD5,
+                                &hash_md5, &hlen_md5);
+    if(rc != SSH_OK || hlen_md5 != 16) {
       failf(data,
             "Denied establishing ssh session: MD5 fingerprint not available");
       goto cleanup;
     }
 
     for(i = 0; i < 16; i++)
-      curl_msnprintf(&md5buffer[i * 2], 3, "%02x", hash[i]);
+      curl_msnprintf(&md5buffer[i * 2], 3, "%02x", hash_md5[i]);
 
     infof(data, "SSH MD5 fingerprint: %s", md5buffer);
 
@@ -297,8 +357,10 @@ cleanup:
     /* !checksrc! disable BANNEDFUNC 1 */
     free(known_base64); /* allocated by libssh, deallocate with system free */
   }
-  if(hash)
-    ssh_clean_pubkey_hash(&hash);
+  if(hash_sha256)
+    ssh_clean_pubkey_hash(&hash_sha256);
+  if(hash_md5)
+    ssh_clean_pubkey_hash(&hash_md5);
   ssh_key_free(pubkey);
   if(knownhostsentry) {
     ssh_knownhosts_entry_free(knownhostsentry);
index 118bc594f6415cd77bffdff547639c0203d1f038..0226ebfd275448d280224d47816b4d5b5b9a918a 100644 (file)
@@ -57,7 +57,7 @@
 #include "curlx/fopen.h"
 #include "vssh/vssh.h"
 #include "curlx/strparse.h"
-#include "curlx/base64.h" /* for base64 encoding/decoding */
+#include "curlx/base64.h" /* for curlx_base64_encode() */
 
 static const char *sftp_libssh2_strerror(unsigned long err)
 {
index 176d3ebc3849f9617329a5a9364b634ef2ee34ab..6c69acd95bf357a8972d8d71c85745a14b954716 100644 (file)
@@ -2768,10 +2768,7 @@ static ParameterError opt_string(struct OperationConfig *config,
     }
     break;
   case C_HOSTPUBSHA256: /* --hostpubsha256 */
-    if(!feature_libssh2)
-      err = PARAM_LIBCURL_DOESNT_SUPPORT;
-    else
-      err = getstr(&config->hostpubsha256, nextarg, DENY_BLANK);
+    err = getstr(&config->hostpubsha256, nextarg, DENY_BLANK);
     break;
   case C_TLSUSER: /* --tlsuser */
     if(!feature_tls_srp)
index 5a5382c00701cce79d2580c4ec3a4a12f810700b..9aee23428090d099276350fc1279e184bc807614 100644 (file)
@@ -71,7 +71,6 @@ bool feature_http2 = FALSE;
 bool feature_http3 = FALSE;
 bool feature_httpsproxy = FALSE;
 bool feature_libz = FALSE;
-bool feature_libssh2 = FALSE;
 bool feature_ntlm = FALSE;
 bool feature_ntlm_wb = FALSE;
 bool feature_spnego = FALSE;
@@ -183,9 +182,6 @@ CURLcode get_libcurl_info(void)
     ++feature_count;
   }
 
-  feature_libssh2 = curlinfo->age >= CURLVERSION_FOURTH &&
-                    curlinfo->libssh_version &&
-                    !strncmp("libssh2", curlinfo->libssh_version, 7);
   return CURLE_OK;
 }
 
index ddc41a133867a57e694702e5abe5996420282386..e8c3517a9b2a2de0abf645c9605064867588be45 100644 (file)
@@ -54,7 +54,6 @@ extern bool feature_http2;
 extern bool feature_http3;
 extern bool feature_httpsproxy;
 extern bool feature_libz;
-extern bool feature_libssh2;
 extern bool feature_ntlm;
 extern bool feature_ntlm_wb;
 extern bool feature_spnego;
index b7fe479cb2bcd1364fdab070d2b59f913ff67f8e..1aa973a688435d34b5926b73f813fd6ee00f89c0 100644 (file)
@@ -17,10 +17,6 @@ test
 
 # Client-side
 <client>
-# so far only the libssh2 backend supports SHA256
-<features>
-libssh2
-</features>
 <server>
 sftp
 </server>
index 057242b0f5136cd24b03c692199e11c7715c8a21..6d692a817ef3ced29be55b76fab9a303e55e726e 100644 (file)
@@ -17,10 +17,6 @@ test
 
 # Client-side
 <client>
-# so far only the libssh2 backend supports SHA256
-<features>
-libssh2
-</features>
 <server>
 scp
 </server>