libssh: add support for SHA256 host public keys

Reported-by: Joshua Rogers
Fixes #21605

Closes #21607
This commit is contained in:
Viktor Szakats 2026-05-14 13:32:46 +02:00
parent 9135294115
commit eb9b253d66
No known key found for this signature in database
11 changed files with 85 additions and 48 deletions

View 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

View 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.

View file

@ -73,10 +73,6 @@ int main(void)
}
~~~
# NOTES
Requires the libssh2 backend.
# %AVAILABILITY%
# RETURN VALUE

View 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

View 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_MD5]) {
int i;
char md5buffer[33];
const char *pubkey_md5 = data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5];
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_MD5, &hash, &hlen);
if(rc != SSH_OK || hlen != 16) {
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]) {
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_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);

View 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)
{

View 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)

View 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;
}

View 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;

View file

@ -17,10 +17,6 @@ test
# Client-side
<client>
# so far only the libssh2 backend supports SHA256
<features>
libssh2
</features>
<server>
sftp
</server>

View file

@ -17,10 +17,6 @@ test
# Client-side
<client>
# so far only the libssh2 backend supports SHA256
<features>
libssh2
</features>
<server>
scp
</server>