summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMats Lindestam <matslm@axis.com>2021-09-26 23:20:53 +0200
committerDaniel Stenberg <daniel@haxx.se>2021-09-26 23:20:53 +0200
commitd1e7d9197b7fe417fb4d62aad5ea8f15a06d906c (patch)
tree549f3d1e4f04ecc8c499aafa4d21bd968b32a461
parent1ca62bb5ce3f37174d4bf3f9f70674c4af4396df (diff)
downloadcurl-d1e7d9197b7fe417fb4d62aad5ea8f15a06d906c.tar.gz
libssh2: add SHA256 fingerprint support
Added support for SHA256 fingerprint in command line curl and in libcurl. Closes #7646
-rw-r--r--docs/TODO10
-rw-r--r--docs/cmdline-opts/Makefile.inc1
-rw-r--r--docs/cmdline-opts/hostpubsha256.d11
-rw-r--r--docs/libcurl/curl_easy_setopt.32
-rw-r--r--docs/libcurl/opts/CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256.360
-rw-r--r--docs/libcurl/opts/Makefile.inc1
-rw-r--r--docs/libcurl/symbols-in-versions1
-rw-r--r--docs/options-in-versions1
-rw-r--r--include/curl/curl.h3
-rw-r--r--include/curl/typecheck-gcc.h1
-rw-r--r--lib/easyoptions.c4
-rw-r--r--lib/setopt.c9
-rw-r--r--lib/urldata.h1
-rw-r--r--lib/vssh/libssh2.c153
-rw-r--r--src/tool_cfgable.c1
-rw-r--r--src/tool_cfgable.h1
-rw-r--r--src/tool_getparam.c4
-rw-r--r--src/tool_help.c3
-rw-r--r--src/tool_operate.c5
-rw-r--r--tests/.gitignore1
-rw-r--r--tests/FILEFORMAT.md1
-rw-r--r--tests/data/Makefile.inc2
-rw-r--r--tests/data/test302144
-rw-r--r--tests/data/test302244
-rwxr-xr-xtests/runtests.pl13
-rw-r--r--tests/sshhelp.pm3
-rw-r--r--tests/sshserver.pl18
27 files changed, 360 insertions, 38 deletions
diff --git a/docs/TODO b/docs/TODO
index 2fca64797..4a9d99805 100644
--- a/docs/TODO
+++ b/docs/TODO
@@ -138,7 +138,6 @@
17. SSH protocols
17.1 Multiplexing
17.2 Handle growing SFTP files
- 17.3 Support better than MD5 hostkey hash
17.4 Support CURLOPT_PREQUOTE
17.5 SSH over HTTPS proxy with more backends
@@ -930,15 +929,6 @@
https://github.com/curl/curl/issues/4344
-17.3 Support better than MD5 hostkey hash
-
- libcurl offers the CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 option for verifying the
- server's key. MD5 is generally being deprecated so we should implement
- support for stronger hashing algorithms. libssh2 itself is what provides this
- underlying functionality and it supports at least SHA-1 as an alternative.
- SHA-1 is also being deprecated these days so we should consider working with
- libssh2 to instead offer support for SHA-256 or similar.
-
17.4 Support CURLOPT_PREQUOTE
The two other QUOTE options are supported for SFTP, but this was left out for
diff --git a/docs/cmdline-opts/Makefile.inc b/docs/cmdline-opts/Makefile.inc
index 6e04552e9..506025a75 100644
--- a/docs/cmdline-opts/Makefile.inc
+++ b/docs/cmdline-opts/Makefile.inc
@@ -96,6 +96,7 @@ DPAGES = \
header.d \
help.d \
hostpubmd5.d \
+ hostpubsha256.d \
hsts.d \
http0.9.d \
http1.0.d \
diff --git a/docs/cmdline-opts/hostpubsha256.d b/docs/cmdline-opts/hostpubsha256.d
new file mode 100644
index 000000000..81e6f9851
--- /dev/null
+++ b/docs/cmdline-opts/hostpubsha256.d
@@ -0,0 +1,11 @@
+Long: hostpubsha256
+Arg: <sha256>
+Help: Acceptable SHA256 hash of the host public key
+Protocols: SFTP SCP
+Added: 7.80.0
+Category: sftp scp
+Example: --hostpubsha256 NDVkMTQxMGQ1ODdmMjQ3MjczYjAyOTY5MmRkMjVmNDQ= sftp://example.com/
+---
+Pass a string containing a Base64-encoded SHA256 hash of the remote
+host's public key. Curl will refuse the connection with the host
+unless the hashes match.
diff --git a/docs/libcurl/curl_easy_setopt.3 b/docs/libcurl/curl_easy_setopt.3
index b83f5b635..592692b94 100644
--- a/docs/libcurl/curl_easy_setopt.3
+++ b/docs/libcurl/curl_easy_setopt.3
@@ -642,6 +642,8 @@ SSH authentication types. See \fICURLOPT_SSH_AUTH_TYPES(3)\fP
Enable SSH compression. See \fICURLOPT_SSH_COMPRESSION(3)\fP
.IP CURLOPT_SSH_HOST_PUBLIC_KEY_MD5
MD5 of host's public key. See \fICURLOPT_SSH_HOST_PUBLIC_KEY_MD5(3)\fP
+.IP CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256
+SHA256 of host's public key. See \fICURLOPT_SSH_HOST_PUBLIC_KEY_SHA256(3)\fP
.IP CURLOPT_SSH_PUBLIC_KEYFILE
File name of public key. See \fICURLOPT_SSH_PUBLIC_KEYFILE(3)\fP
.IP CURLOPT_SSH_PRIVATE_KEYFILE
diff --git a/docs/libcurl/opts/CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256.3 b/docs/libcurl/opts/CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256.3
new file mode 100644
index 000000000..30be32ef2
--- /dev/null
+++ b/docs/libcurl/opts/CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256.3
@@ -0,0 +1,60 @@
+.\" **************************************************************************
+.\" * _ _ ____ _
+.\" * Project ___| | | | _ \| |
+.\" * / __| | | | |_) | |
+.\" * | (__| |_| | _ <| |___
+.\" * \___|\___/|_| \_\_____|
+.\" *
+.\" * Copyright (C) 1998 - 2021, Daniel Stenberg, <daniel@haxx.se>, et al.
+.\" *
+.\" * This software is licensed as described in the file COPYING, which
+.\" * you should have received as part of this distribution. The terms
+.\" * are also available at https://curl.se/docs/copyright.html.
+.\" *
+.\" * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+.\" * copies of the Software, and permit persons to whom the Software is
+.\" * furnished to do so, under the terms of the COPYING file.
+.\" *
+.\" * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+.\" * KIND, either express or implied.
+.\" *
+.\" **************************************************************************
+.\"
+.TH CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 3 "27 Aug 2021" "libcurl 7.80.0" "curl_easy_setopt options"
+.SH NAME
+CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 \- SHA256 hash of SSH server public key
+.SH SYNOPSIS
+.nf
+#include <curl/curl.h>
+
+CURLcode curl_easy_setopt(CURL *handle, CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256,
+ char *sha256);
+.SH DESCRIPTION
+Pass a char * pointing to a string containing a Base64-encoded SHA256
+hash of the remote host's public key.
+The transfer will fail if the given hash doesn't match the hash the
+remote host provides.
+
+.SH DEFAULT
+NULL
+.SH PROTOCOLS
+SCP and SFTP
+.SH EXAMPLE
+.nf
+CURL *curl = curl_easy_init();
+if(curl) {
+ curl_easy_setopt(curl, CURLOPT_URL, "sftp://example.com/file");
+ curl_easy_setopt(curl, CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256,
+ "NDVkMTQxMGQ1ODdmMjQ3MjczYjAyOTY5MmRkMjVmNDQ=");
+ ret = curl_easy_perform(curl);
+ curl_easy_cleanup(curl);
+}
+.fi
+.SH AVAILABILITY
+Added in 7.80.0
+Requires the libssh2 back-end.
+.SH RETURN VALUE
+Returns CURLE_OK if the option is supported, CURLE_UNKNOWN_OPTION if not, or
+CURLE_OUT_OF_MEMORY if there was insufficient heap space.
+.SH "SEE ALSO"
+.BR CURLOPT_SSH_PUBLIC_KEYFILE "(3), " CURLOPT_SSH_AUTH_TYPES "(3), "
diff --git a/docs/libcurl/opts/Makefile.inc b/docs/libcurl/opts/Makefile.inc
index 1181331b9..4ef5ddf43 100644
--- a/docs/libcurl/opts/Makefile.inc
+++ b/docs/libcurl/opts/Makefile.inc
@@ -326,6 +326,7 @@ man_MANS = \
CURLOPT_SSH_AUTH_TYPES.3 \
CURLOPT_SSH_COMPRESSION.3 \
CURLOPT_SSH_HOST_PUBLIC_KEY_MD5.3 \
+ CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256.3 \
CURLOPT_SSH_KEYDATA.3 \
CURLOPT_SSH_KEYFUNCTION.3 \
CURLOPT_SSH_KNOWNHOSTS.3 \
diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions
index 55f25b6b0..8be22d504 100644
--- a/docs/libcurl/symbols-in-versions
+++ b/docs/libcurl/symbols-in-versions
@@ -613,6 +613,7 @@ CURLOPT_SOURCE_USERPWD 7.12.1 - 7.15.5
CURLOPT_SSH_AUTH_TYPES 7.16.1
CURLOPT_SSH_COMPRESSION 7.56.0
CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 7.17.1
+CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 7.80.0
CURLOPT_SSH_KEYDATA 7.19.6
CURLOPT_SSH_KEYFUNCTION 7.19.6
CURLOPT_SSH_KNOWNHOSTS 7.19.6
diff --git a/docs/options-in-versions b/docs/options-in-versions
index 6fada9381..ac087a1ef 100644
--- a/docs/options-in-versions
+++ b/docs/options-in-versions
@@ -84,6 +84,7 @@
--header (-H) 5.0
--help (-h) 4.0
--hostpubmd5 7.17.1
+--hostpubsha256 7.80.0
--hsts 7.74.0
--http0.9 7.64.0
--http1.0 (-0) 7.9.1
diff --git a/include/curl/curl.h b/include/curl/curl.h
index 835c3d871..6eb0fcb82 100644
--- a/include/curl/curl.h
+++ b/include/curl/curl.h
@@ -2102,6 +2102,9 @@ typedef enum {
this option is used only if PROXY_SSL_VERIFYPEER is true */
CURLOPT(CURLOPT_PROXY_CAINFO_BLOB, CURLOPTTYPE_BLOB, 310),
+ /* used by scp/sftp to verify the host's public key */
+ CURLOPT(CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256, CURLOPTTYPE_STRINGPOINT, 311),
+
CURLOPT_LASTENTRY /* the last unused */
} CURLoption;
diff --git a/include/curl/typecheck-gcc.h b/include/curl/typecheck-gcc.h
index 34d0267ed..77ce461bc 100644
--- a/include/curl/typecheck-gcc.h
+++ b/include/curl/typecheck-gcc.h
@@ -317,6 +317,7 @@ CURLWARNING(_curl_easy_getinfo_err_curl_off_t,
(option) == CURLOPT_SERVICE_NAME || \
(option) == CURLOPT_SOCKS5_GSSAPI_SERVICE || \
(option) == CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 || \
+ (option) == CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 || \
(option) == CURLOPT_SSH_KNOWNHOSTS || \
(option) == CURLOPT_SSH_PRIVATE_KEYFILE || \
(option) == CURLOPT_SSH_PUBLIC_KEYFILE || \
diff --git a/lib/easyoptions.c b/lib/easyoptions.c
index 4e65e3525..b1c0704d5 100644
--- a/lib/easyoptions.c
+++ b/lib/easyoptions.c
@@ -271,6 +271,8 @@ struct curl_easyoption Curl_easyopts[] = {
{"SSH_COMPRESSION", CURLOPT_SSH_COMPRESSION, CURLOT_LONG, 0},
{"SSH_HOST_PUBLIC_KEY_MD5", CURLOPT_SSH_HOST_PUBLIC_KEY_MD5,
CURLOT_STRING, 0},
+ {"SSH_HOST_PUBLIC_KEY_SHA256", CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256,
+ CURLOT_STRING, 0},
{"SSH_KEYDATA", CURLOPT_SSH_KEYDATA, CURLOT_CBPTR, 0},
{"SSH_KEYFUNCTION", CURLOPT_SSH_KEYFUNCTION, CURLOT_FUNCTION, 0},
{"SSH_KNOWNHOSTS", CURLOPT_SSH_KNOWNHOSTS, CURLOT_STRING, 0},
@@ -354,6 +356,6 @@ struct curl_easyoption Curl_easyopts[] = {
*/
int Curl_easyopts_check(void)
{
- return ((CURLOPT_LASTENTRY%10000) != (310 + 1));
+ return ((CURLOPT_LASTENTRY%10000) != (311 + 1));
}
#endif
diff --git a/lib/setopt.c b/lib/setopt.c
index 08827d1ef..c62a62fb5 100644
--- a/lib/setopt.c
+++ b/lib/setopt.c
@@ -2477,6 +2477,15 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
va_arg(param, char *));
break;
+ case CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256:
+ /*
+ * Option to allow for the SHA256 of the host public key to be checked
+ * for validation purposes.
+ */
+ result = Curl_setstropt(&data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256],
+ va_arg(param, char *));
+ break;
+
case CURLOPT_SSH_KNOWNHOSTS:
/*
* Store the file name to read known hosts from.
diff --git a/lib/urldata.h b/lib/urldata.h
index 6ffd97621..2d1e873a5 100644
--- a/lib/urldata.h
+++ b/lib/urldata.h
@@ -1554,6 +1554,7 @@ enum dupstring {
STRING_SSH_PRIVATE_KEY, /* path to the private key file for auth */
STRING_SSH_PUBLIC_KEY, /* path to the public key file for auth */
STRING_SSH_HOST_PUBLIC_KEY_MD5, /* md5 of host public key in ascii hex */
+ STRING_SSH_HOST_PUBLIC_KEY_SHA256, /* sha256 of host public key in base64 */
STRING_SSH_KNOWNHOSTS, /* file name of knownhosts file */
STRING_PROXY_SERVICE_NAME, /* Proxy service name */
STRING_SERVICE_NAME, /* Service name */
diff --git a/lib/vssh/libssh2.c b/lib/vssh/libssh2.c
index 8ccfe68a8..7466840ff 100644
--- a/lib/vssh/libssh2.c
+++ b/lib/vssh/libssh2.c
@@ -81,6 +81,11 @@
#include "select.h"
#include "warnless.h"
#include "curl_path.h"
+#include "strcase.h"
+
+#include <curl_base64.h> /* for base64 encoding/decoding */
+#include <curl_sha256.h>
+
/* The last 3 #include files should be in this order */
#include "curl_printf.h"
@@ -615,40 +620,142 @@ static CURLcode ssh_check_fingerprint(struct Curl_easy *data)
struct connectdata *conn = data->conn;
struct ssh_conn *sshc = &conn->proto.sshc;
const char *pubkey_md5 = data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5];
- char md5buffer[33];
+ const char *pubkey_sha256 = data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256];
+
+ infof(data, "SSH MD5 public key: %s",
+ pubkey_md5 != NULL ? pubkey_md5 : "NULL");
+ infof(data, "SSH SHA256 public key: %s",
+ pubkey_sha256 != NULL ? pubkey_sha256 : "NULL");
- const char *fingerprint = libssh2_hostkey_hash(sshc->ssh_session,
- LIBSSH2_HOSTKEY_HASH_MD5);
+ if(pubkey_sha256) {
+ const char *fingerprint = NULL;
+ char *fingerprint_b64 = NULL;
+ size_t fingerprint_b64_len;
+ size_t pub_pos = 0;
+ size_t b64_pos = 0;
- if(fingerprint) {
+#ifdef LIBSSH2_HOSTKEY_HASH_SHA256
/* The fingerprint points to static storage (!), don't free() it. */
- int i;
- for(i = 0; i < 16; i++)
- msnprintf(&md5buffer[i*2], 3, "%02x", (unsigned char) fingerprint[i]);
- infof(data, "SSH MD5 fingerprint: %s", md5buffer);
- }
+ fingerprint = libssh2_hostkey_hash(sshc->ssh_session,
+ LIBSSH2_HOSTKEY_HASH_SHA256);
+#else
+ const char *hostkey;
+ size_t len = 0;
+ unsigned char hash[32];
+
+ hostkey = libssh2_session_hostkey(sshc->ssh_session, &len, NULL);
+ if(hostkey) {
+ Curl_sha256it(hash, (const unsigned char *) hostkey, len);
+ fingerprint = (char *) hash;
+ }
+#endif
- /* Before we authenticate we check the hostkey's MD5 fingerprint
- * against a known fingerprint, if available.
- */
- if(pubkey_md5 && strlen(pubkey_md5) == 32) {
- if(!fingerprint || !strcasecompare(md5buffer, pubkey_md5)) {
- if(fingerprint)
- failf(data,
- "Denied establishing ssh session: mismatch md5 fingerprint. "
- "Remote %s is not equal to %s", md5buffer, pubkey_md5);
- else
- failf(data,
- "Denied establishing ssh session: md5 fingerprint not available");
+ if(!fingerprint) {
+ failf(data,
+ "Denied establishing ssh session: sha256 fingerprint "
+ "not available");
+ state(data, SSH_SESSION_FREE);
+ sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
+ return sshc->actualcode;
+ }
+
+ /* The length of fingerprint is 32 bytes for SHA256.
+ * See libssh2_hostkey_hash documentation. */
+ if(Curl_base64_encode (data, fingerprint, 32, &fingerprint_b64,
+ &fingerprint_b64_len) != CURLE_OK) {
+ state(data, SSH_SESSION_FREE);
+ sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
+ return sshc->actualcode;
+ }
+
+ if(!fingerprint_b64) {
+ failf(data,
+ "sha256 fingerprint could not be encoded");
+ state(data, SSH_SESSION_FREE);
+ sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
+ return sshc->actualcode;
+ }
+
+ 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) ||
+ Curl_strncasecompare(fingerprint_b64, pubkey_sha256, pub_pos) != 1) {
+ free(fingerprint_b64);
+
+ failf(data,
+ "Denied establishing ssh session: mismatch sha256 fingerprint. "
+ "Remote %s is not equal to %s", fingerprint, pubkey_sha256);
state(data, SSH_SESSION_FREE);
sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
return sshc->actualcode;
}
- infof(data, "MD5 checksum match!");
+
+ free(fingerprint_b64);
+
+ infof(data, "SHA256 checksum match!");
+ }
+
+ if(pubkey_md5) {
+ char md5buffer[33];
+ const char *fingerprint = NULL;
+
+ fingerprint = libssh2_hostkey_hash(sshc->ssh_session,
+ LIBSSH2_HOSTKEY_HASH_MD5);
+
+ if(fingerprint) {
+ /* The fingerprint points to static storage (!), don't free() it. */
+ int i;
+ for(i = 0; i < 16; i++) {
+ msnprintf(&md5buffer[i*2], 3, "%02x", (unsigned char) fingerprint[i]);
+ }
+
+ infof(data, "SSH MD5 fingerprint: %s", md5buffer);
+ }
+
+ /* Before we authenticate we check the hostkey's MD5 fingerprint
+ * against a known fingerprint, if available.
+ */
+ if(pubkey_md5 && strlen(pubkey_md5) == 32) {
+ if(!fingerprint || !strcasecompare(md5buffer, pubkey_md5)) {
+ if(fingerprint) {
+ failf(data,
+ "Denied establishing ssh session: mismatch md5 fingerprint. "
+ "Remote %s is not equal to %s", md5buffer, pubkey_md5);
+ }
+ else {
+ failf(data,
+ "Denied establishing ssh session: md5 fingerprint "
+ "not available");
+ }
+ state(data, SSH_SESSION_FREE);
+ sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
+ return sshc->actualcode;
+ }
+ infof(data, "MD5 checksum match!");
+ }
+ }
+
+ if(!pubkey_md5 && !pubkey_sha256) {
+ return ssh_knownhost(data);
+ }
+ else {
/* as we already matched, we skip the check for known hosts */
return CURLE_OK;
}
- return ssh_knownhost(data);
}
/*
diff --git a/src/tool_cfgable.c b/src/tool_cfgable.c
index c3f7cecb1..34e17ce55 100644
--- a/src/tool_cfgable.c
+++ b/src/tool_cfgable.c
@@ -131,6 +131,7 @@ static void free_config_fields(struct OperationConfig *config)
Curl_safefree(config->proxy_key_passwd);
Curl_safefree(config->pubkey);
Curl_safefree(config->hostpubmd5);
+ Curl_safefree(config->hostpubsha256);
Curl_safefree(config->engine);
Curl_safefree(config->etag_save_file);
Curl_safefree(config->etag_compare_file);
diff --git a/src/tool_cfgable.h b/src/tool_cfgable.h
index b00aacb76..eff55f95d 100644
--- a/src/tool_cfgable.h
+++ b/src/tool_cfgable.h
@@ -158,6 +158,7 @@ struct OperationConfig {
char *proxy_key_passwd;
char *pubkey;
char *hostpubmd5;
+ char *hostpubsha256;
char *engine;
char *etag_save_file;
char *etag_compare_file;
diff --git a/src/tool_getparam.c b/src/tool_getparam.c
index 73ba8f537..1de79634c 100644
--- a/src/tool_getparam.c
+++ b/src/tool_getparam.c
@@ -241,6 +241,7 @@ static const struct LongShort aliases[]= {
{"Eg", "capath", ARG_FILENAME},
{"Eh", "pubkey", ARG_STRING},
{"Ei", "hostpubmd5", ARG_STRING},
+ {"EF", "hostpubsha256", ARG_STRING},
{"Ej", "crlfile", ARG_FILENAME},
{"Ek", "tlsuser", ARG_STRING},
{"El", "tlspassword", ARG_STRING},
@@ -1602,6 +1603,9 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */
if(!config->hostpubmd5 || strlen(config->hostpubmd5) != 32)
return PARAM_BAD_USE;
break;
+ case 'F': /* --hostpubsha256 sha256 of the host public key */
+ GetStr(&config->hostpubsha256, nextarg);
+ break;
case 'j': /* CRL file */
GetStr(&config->crlfile, nextarg);
break;
diff --git a/src/tool_help.c b/src/tool_help.c
index cc23b12e2..46969e77f 100644
--- a/src/tool_help.c
+++ b/src/tool_help.c
@@ -346,6 +346,9 @@ static const struct helptxt helptext[] = {
{" --hostpubmd5 <md5>",
"Acceptable MD5 hash of the host public key",
CURLHELP_SFTP | CURLHELP_SCP},
+ {" --hostpubsha256 <sha256>",
+ "Acceptable SHA256 hash of the host public key",
+ CURLHELP_SFTP | CURLHELP_SCP},
{" --hsts <file name>",
"Enable HSTS with this cache file",
CURLHELP_HTTP},
diff --git a/src/tool_operate.c b/src/tool_operate.c
index ca53d29f7..5d24ccb7d 100644
--- a/src/tool_operate.c
+++ b/src/tool_operate.c
@@ -1408,6 +1408,11 @@ static CURLcode single_transfer(struct GlobalConfig *global,
my_setopt_str(curl, CURLOPT_SSH_HOST_PUBLIC_KEY_MD5,
config->hostpubmd5);
+ /* new in libcurl 7.80.0: SSH host key sha256 checking allows us
+ to fail if we are not talking to who we think we should */
+ my_setopt_str(curl, CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256,
+ config->hostpubsha256);
+
/* new in libcurl 7.56.0 */
if(config->ssh_compression)
my_setopt(curl, CURLOPT_SSH_COMPRESSION, 1L);
diff --git a/tests/.gitignore b/tests/.gitignore
index 00f787cc8..a8882b275 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -7,6 +7,7 @@ curl_client_knownhosts
curl_host_rsa_key
curl_host_rsa_key.pub
curl_host_rsa_key.pub_md5
+curl_host_rsa_key.pub_sha256
curl_sftp_cmds
curl_sftp_config
curl_ssh_config
diff --git a/tests/FILEFORMAT.md b/tests/FILEFORMAT.md
index 7a9c48204..df61c412f 100644
--- a/tests/FILEFORMAT.md
+++ b/tests/FILEFORMAT.md
@@ -148,6 +148,7 @@ Available substitute variables include:
- `%SRCDIR` - Full path to the source dir
- `%SSHPORT` - Port number of the SCP/SFTP server
- `%SSHSRVMD5` - MD5 of SSH server's public key
+- `%SSHSRVSHA256` - SHA256 of SSH server's public key
- `%SSH_PWD` - Current directory friendly for the SSH server
- `%TESTNUMBER` - Number of the test case
- `%TFTP6PORT` - IPv6 port number of the TFTP server
diff --git a/tests/data/Makefile.inc b/tests/data/Makefile.inc
index 1f774ce4e..d906ca338 100644
--- a/tests/data/Makefile.inc
+++ b/tests/data/Makefile.inc
@@ -237,4 +237,4 @@ test2200 test2201 test2202 test2203 test2204 test2205 \
\
test3000 test3001 test3002 test3003 test3004 test3005 test3006 test3007 \
test3008 test3009 test3010 test3011 test3012 test3013 test3014 test3015 \
-test3016 test3017 test3018 test3019 test3020
+test3016 test3017 test3018 test3019 test3020 test3021 test3022
diff --git a/tests/data/test3021 b/tests/data/test3021
new file mode 100644
index 000000000..0a02e1844
--- /dev/null
+++ b/tests/data/test3021
@@ -0,0 +1,44 @@
+<testcase>
+<info>
+<keywords>
+SFTP
+server sha256 key check
+</keywords>
+</info>
+
+#
+# Server-side
+<reply>
+<data>
+test
+</data>
+</reply>
+
+#
+# Client-side
+<client>
+<server>
+sftp
+</server>
+ <name>
+SFTP correct sha256 host key
+ </name>
+ <command>
+--hostpubsha256 %SSHSRVSHA256 --key curl_client_key --pubkey curl_client_key.pub -u %USER: sftp://%HOSTIP:%SSHPORT%SSH_PWD/log/file%TESTNUMBER.txt
+</command>
+<file name="log/file%TESTNUMBER.txt">
+test
+</file>
+</client>
+
+#
+# Verify data after the test has been "shot"
+<verify>
+<errorcode>
+0
+</errorcode>
+<valgrind>
+disable
+</valgrind>
+</verify>
+</testcase>
diff --git a/tests/data/test3022 b/tests/data/test3022
new file mode 100644
index 000000000..f3477909d
--- /dev/null
+++ b/tests/data/test3022
@@ -0,0 +1,44 @@
+<testcase>
+<info>
+<keywords>
+SCP
+server sha256 key check
+</keywords>
+</info>
+
+#
+# Server-side
+<reply>
+<data>
+test
+</data>
+</reply>
+
+#
+# Client-side
+<client>
+<server>
+scp
+</server>
+ <name>
+SCP correct sha256 host key
+ </name>
+ <command>
+--hostpubsha256 %SSHSRVSHA256 --key curl_client_key --pubkey curl_client_key.pub -u %USER: scp://%HOSTIP:%SSHPORT%SSH_PWD/log/file%TESTNUMBER.txt
+</command>
+<file name="log/file%TESTNUMBER.txt">
+test
+</file>
+</client>
+
+#
+# Verify data after the test has been "shot"
+<verify>
+<errorcode>
+0
+</errorcode>
+<valgrind>
+disable
+</valgrind>
+</verify>
+</testcase>
diff --git a/tests/runtests.pl b/tests/runtests.pl
index 38b76e878..9a1c169b6 100755
--- a/tests/runtests.pl
+++ b/tests/runtests.pl
@@ -168,6 +168,7 @@ my $proxy_address;
my %custom_skip_reasons;
my $SSHSRVMD5 = "[uninitialized]"; # MD5 of ssh server public key
+my $SSHSRVSHA256 = "[uninitialized]"; # SHA256 of ssh server public key
my $VERSION=""; # curl's reported version number
my $srcdir = $ENV{'srcdir'} || '.';
@@ -2287,6 +2288,17 @@ sub runsshserver {
die $msg;
}
+ my $hstpubsha256f = "curl_host_rsa_key.pub_sha256";
+ if(!open(PUBSHA256FILE, "<", $hstpubsha256f) ||
+ (read(PUBSHA256FILE, $SSHSRVSHA256, 48) == 0) ||
+ !close(PUBSHA256FILE))
+ {
+ my $msg = "Fatal: $srvrname pubkey sha256 missing : \"$hstpubsha256f\" : $!";
+ logmsg "$msg\n";
+ stopservers($verbose);
+ die $msg;
+ }
+
logmsg "RUN: $srvrname on PID $pid2 port $wport\n" if($verbose);
return ($pid2, $sshpid, $wport);
@@ -3374,6 +3386,7 @@ sub subVariables {
$$thing =~ s/${prefix}USER/$USER/g;
$$thing =~ s/${prefix}SSHSRVMD5/$SSHSRVMD5/g;
+ $$thing =~ s/${prefix}SSHSRVSHA256/$SSHSRVSHA256/g;
# The purpose of FTPTIME2 and FTPTIME3 is to provide times that can be
# used for time-out tests and that would work on most hosts as these
diff --git a/tests/sshhelp.pm b/tests/sshhelp.pm
index 0f71b3079..41047e9c6 100644
--- a/tests/sshhelp.pm
+++ b/tests/sshhelp.pm
@@ -51,6 +51,7 @@ use vars qw(
$hstprvkeyf
$hstpubkeyf
$hstpubmd5f
+ $hstpubsha256f
$cliprvkeyf
$clipubkeyf
@sftppath
@@ -84,6 +85,7 @@ use vars qw(
$hstprvkeyf
$hstpubkeyf
$hstpubmd5f
+ $hstpubsha256f
$cliprvkeyf
$clipubkeyf
display_sshdconfig
@@ -125,6 +127,7 @@ $knownhosts = 'curl_client_knownhosts'; # ssh knownhosts file
$hstprvkeyf = 'curl_host_rsa_key'; # host private key file
$hstpubkeyf = 'curl_host_rsa_key.pub'; # host public key file
$hstpubmd5f = 'curl_host_rsa_key.pub_md5'; # md5 hash of host public key
+$hstpubsha256f = 'curl_host_rsa_key.pub_sha256'; # sha256 hash of host public key
$cliprvkeyf = 'curl_client_key'; # client private key file
$clipubkeyf = 'curl_client_key.pub'; # client public key file
diff --git a/tests/sshserver.pl b/tests/sshserver.pl
index 412cab33e..526ed099f 100644
--- a/tests/sshserver.pl
+++ b/tests/sshserver.pl
@@ -30,6 +30,8 @@ use Cwd;
use Cwd 'abs_path';
use Digest::MD5;
use Digest::MD5 'md5_hex';
+use Digest::SHA;
+use Digest::SHA 'sha256_base64';
use MIME::Base64;
#***************************************************************************
@@ -52,6 +54,7 @@ use sshhelp qw(
$hstprvkeyf
$hstpubkeyf
$hstpubmd5f
+ $hstpubsha256f
$cliprvkeyf
$clipubkeyf
display_sshdconfig
@@ -362,10 +365,12 @@ if((($sshid =~ /OpenSSH/) && ($sshvernum < 299)) ||
if((! -e $hstprvkeyf) || (! -s $hstprvkeyf) ||
(! -e $hstpubkeyf) || (! -s $hstpubkeyf) ||
(! -e $hstpubmd5f) || (! -s $hstpubmd5f) ||
+ (! -e $hstpubsha256f) || (! -s $hstpubsha256f) ||
(! -e $cliprvkeyf) || (! -s $cliprvkeyf) ||
(! -e $clipubkeyf) || (! -s $clipubkeyf)) {
# Make sure all files are gone so ssh-keygen doesn't complain
- unlink($hstprvkeyf, $hstpubkeyf, $hstpubmd5f, $cliprvkeyf, $clipubkeyf);
+ unlink($hstprvkeyf, $hstpubkeyf, $hstpubmd5f, $hstpubsha256f,
+ $cliprvkeyf, $clipubkeyf);
logmsg 'generating host keys...' if($verbose);
if(system "\"$sshkeygen\" -q -t rsa -f $hstprvkeyf -C 'curl test server' -N ''") {
logmsg 'Could not generate host key';
@@ -379,7 +384,7 @@ if((! -e $hstprvkeyf) || (! -s $hstprvkeyf) ||
# Make sure that permissions are restricted so openssh doesn't complain
system "chmod 600 $hstprvkeyf";
system "chmod 600 $cliprvkeyf";
- # Save md5 hash of public host key
+ # Save md5 and sha256 hashes of public host key
open(RSAKEYFILE, "<$hstpubkeyf");
my @rsahostkey = do { local $/ = ' '; <RSAKEYFILE> };
close(RSAKEYFILE);
@@ -394,6 +399,13 @@ if((! -e $hstprvkeyf) || (! -s $hstprvkeyf) ||
logmsg 'Failed writing md5 hash of RSA host key';
exit 1;
}
+ open(PUBSHA256FILE, ">$hstpubsha256f");
+ print PUBSHA256FILE sha256_base64(decode_base64($rsahostkey[1]));
+ close(PUBSHA256FILE);
+ if((! -e $hstpubsha256f) || (! -s $hstpubsha256f)) {
+ logmsg 'Failed writing sha256 hash of RSA host key';
+ exit 1;
+ }
}
@@ -1141,7 +1153,7 @@ elsif($verbose && ($rc >> 8)) {
#***************************************************************************
# Clean up once the server has stopped
#
-unlink($hstprvkeyf, $hstpubkeyf, $hstpubmd5f,
+unlink($hstprvkeyf, $hstpubkeyf, $hstpubmd5f, $hstpubsha256f,
$cliprvkeyf, $clipubkeyf, $knownhosts,
$sshdconfig, $sshconfig, $sftpconfig);