Skip to content

Commit d1e7d91

Browse files
Axis-Matsbagder
authored andcommitted
libssh2: add SHA256 fingerprint support
Added support for SHA256 fingerprint in command line curl and in libcurl. Closes curl#7646
1 parent 1ca62bb commit d1e7d91

27 files changed

+360
-38
lines changed

docs/TODO

-10
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@
138138
17. SSH protocols
139139
17.1 Multiplexing
140140
17.2 Handle growing SFTP files
141-
17.3 Support better than MD5 hostkey hash
142141
17.4 Support CURLOPT_PREQUOTE
143142
17.5 SSH over HTTPS proxy with more backends
144143

@@ -930,15 +929,6 @@
930929

931930
https://github.com/curl/curl/issues/4344
932931

933-
17.3 Support better than MD5 hostkey hash
934-
935-
libcurl offers the CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 option for verifying the
936-
server's key. MD5 is generally being deprecated so we should implement
937-
support for stronger hashing algorithms. libssh2 itself is what provides this
938-
underlying functionality and it supports at least SHA-1 as an alternative.
939-
SHA-1 is also being deprecated these days so we should consider working with
940-
libssh2 to instead offer support for SHA-256 or similar.
941-
942932
17.4 Support CURLOPT_PREQUOTE
943933

944934
The two other QUOTE options are supported for SFTP, but this was left out for

docs/cmdline-opts/Makefile.inc

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ DPAGES = \
9696
header.d \
9797
help.d \
9898
hostpubmd5.d \
99+
hostpubsha256.d \
99100
hsts.d \
100101
http0.9.d \
101102
http1.0.d \

docs/cmdline-opts/hostpubsha256.d

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Long: hostpubsha256
2+
Arg: <sha256>
3+
Help: Acceptable SHA256 hash of the host public key
4+
Protocols: SFTP SCP
5+
Added: 7.80.0
6+
Category: sftp scp
7+
Example: --hostpubsha256 NDVkMTQxMGQ1ODdmMjQ3MjczYjAyOTY5MmRkMjVmNDQ= sftp://example.com/
8+
---
9+
Pass a string containing a Base64-encoded SHA256 hash of the remote
10+
host's public key. Curl will refuse the connection with the host
11+
unless the hashes match.

docs/libcurl/curl_easy_setopt.3

+2
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,8 @@ SSH authentication types. See \fICURLOPT_SSH_AUTH_TYPES(3)\fP
642642
Enable SSH compression. See \fICURLOPT_SSH_COMPRESSION(3)\fP
643643
.IP CURLOPT_SSH_HOST_PUBLIC_KEY_MD5
644644
MD5 of host's public key. See \fICURLOPT_SSH_HOST_PUBLIC_KEY_MD5(3)\fP
645+
.IP CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256
646+
SHA256 of host's public key. See \fICURLOPT_SSH_HOST_PUBLIC_KEY_SHA256(3)\fP
645647
.IP CURLOPT_SSH_PUBLIC_KEYFILE
646648
File name of public key. See \fICURLOPT_SSH_PUBLIC_KEYFILE(3)\fP
647649
.IP CURLOPT_SSH_PRIVATE_KEYFILE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
.\" **************************************************************************
2+
.\" * _ _ ____ _
3+
.\" * Project ___| | | | _ \| |
4+
.\" * / __| | | | |_) | |
5+
.\" * | (__| |_| | _ <| |___
6+
.\" * \___|\___/|_| \_\_____|
7+
.\" *
8+
.\" * Copyright (C) 1998 - 2021, Daniel Stenberg, <[email protected]>, et al.
9+
.\" *
10+
.\" * This software is licensed as described in the file COPYING, which
11+
.\" * you should have received as part of this distribution. The terms
12+
.\" * are also available at https://curl.se/docs/copyright.html.
13+
.\" *
14+
.\" * You may opt to use, copy, modify, merge, publish, distribute and/or sell
15+
.\" * copies of the Software, and permit persons to whom the Software is
16+
.\" * furnished to do so, under the terms of the COPYING file.
17+
.\" *
18+
.\" * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
19+
.\" * KIND, either express or implied.
20+
.\" *
21+
.\" **************************************************************************
22+
.\"
23+
.TH CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 3 "27 Aug 2021" "libcurl 7.80.0" "curl_easy_setopt options"
24+
.SH NAME
25+
CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 \- SHA256 hash of SSH server public key
26+
.SH SYNOPSIS
27+
.nf
28+
#include <curl/curl.h>
29+
30+
CURLcode curl_easy_setopt(CURL *handle, CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256,
31+
char *sha256);
32+
.SH DESCRIPTION
33+
Pass a char * pointing to a string containing a Base64-encoded SHA256
34+
hash of the remote host's public key.
35+
The transfer will fail if the given hash doesn't match the hash the
36+
remote host provides.
37+
38+
.SH DEFAULT
39+
NULL
40+
.SH PROTOCOLS
41+
SCP and SFTP
42+
.SH EXAMPLE
43+
.nf
44+
CURL *curl = curl_easy_init();
45+
if(curl) {
46+
curl_easy_setopt(curl, CURLOPT_URL, "sftp://example.com/file");
47+
curl_easy_setopt(curl, CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256,
48+
"NDVkMTQxMGQ1ODdmMjQ3MjczYjAyOTY5MmRkMjVmNDQ=");
49+
ret = curl_easy_perform(curl);
50+
curl_easy_cleanup(curl);
51+
}
52+
.fi
53+
.SH AVAILABILITY
54+
Added in 7.80.0
55+
Requires the libssh2 back-end.
56+
.SH RETURN VALUE
57+
Returns CURLE_OK if the option is supported, CURLE_UNKNOWN_OPTION if not, or
58+
CURLE_OUT_OF_MEMORY if there was insufficient heap space.
59+
.SH "SEE ALSO"
60+
.BR CURLOPT_SSH_PUBLIC_KEYFILE "(3), " CURLOPT_SSH_AUTH_TYPES "(3), "

docs/libcurl/opts/Makefile.inc

+1
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ man_MANS = \
326326
CURLOPT_SSH_AUTH_TYPES.3 \
327327
CURLOPT_SSH_COMPRESSION.3 \
328328
CURLOPT_SSH_HOST_PUBLIC_KEY_MD5.3 \
329+
CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256.3 \
329330
CURLOPT_SSH_KEYDATA.3 \
330331
CURLOPT_SSH_KEYFUNCTION.3 \
331332
CURLOPT_SSH_KNOWNHOSTS.3 \

docs/libcurl/symbols-in-versions

+1
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,7 @@ CURLOPT_SOURCE_USERPWD 7.12.1 - 7.15.5
613613
CURLOPT_SSH_AUTH_TYPES 7.16.1
614614
CURLOPT_SSH_COMPRESSION 7.56.0
615615
CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 7.17.1
616+
CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 7.80.0
616617
CURLOPT_SSH_KEYDATA 7.19.6
617618
CURLOPT_SSH_KEYFUNCTION 7.19.6
618619
CURLOPT_SSH_KNOWNHOSTS 7.19.6

docs/options-in-versions

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
--header (-H) 5.0
8585
--help (-h) 4.0
8686
--hostpubmd5 7.17.1
87+
--hostpubsha256 7.80.0
8788
--hsts 7.74.0
8889
--http0.9 7.64.0
8990
--http1.0 (-0) 7.9.1

include/curl/curl.h

+3
Original file line numberDiff line numberDiff line change
@@ -2102,6 +2102,9 @@ typedef enum {
21022102
this option is used only if PROXY_SSL_VERIFYPEER is true */
21032103
CURLOPT(CURLOPT_PROXY_CAINFO_BLOB, CURLOPTTYPE_BLOB, 310),
21042104

2105+
/* used by scp/sftp to verify the host's public key */
2106+
CURLOPT(CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256, CURLOPTTYPE_STRINGPOINT, 311),
2107+
21052108
CURLOPT_LASTENTRY /* the last unused */
21062109
} CURLoption;
21072110

include/curl/typecheck-gcc.h

+1
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ CURLWARNING(_curl_easy_getinfo_err_curl_off_t,
317317
(option) == CURLOPT_SERVICE_NAME || \
318318
(option) == CURLOPT_SOCKS5_GSSAPI_SERVICE || \
319319
(option) == CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 || \
320+
(option) == CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 || \
320321
(option) == CURLOPT_SSH_KNOWNHOSTS || \
321322
(option) == CURLOPT_SSH_PRIVATE_KEYFILE || \
322323
(option) == CURLOPT_SSH_PUBLIC_KEYFILE || \

lib/easyoptions.c

+3-1
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ struct curl_easyoption Curl_easyopts[] = {
271271
{"SSH_COMPRESSION", CURLOPT_SSH_COMPRESSION, CURLOT_LONG, 0},
272272
{"SSH_HOST_PUBLIC_KEY_MD5", CURLOPT_SSH_HOST_PUBLIC_KEY_MD5,
273273
CURLOT_STRING, 0},
274+
{"SSH_HOST_PUBLIC_KEY_SHA256", CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256,
275+
CURLOT_STRING, 0},
274276
{"SSH_KEYDATA", CURLOPT_SSH_KEYDATA, CURLOT_CBPTR, 0},
275277
{"SSH_KEYFUNCTION", CURLOPT_SSH_KEYFUNCTION, CURLOT_FUNCTION, 0},
276278
{"SSH_KNOWNHOSTS", CURLOPT_SSH_KNOWNHOSTS, CURLOT_STRING, 0},
@@ -354,6 +356,6 @@ struct curl_easyoption Curl_easyopts[] = {
354356
*/
355357
int Curl_easyopts_check(void)
356358
{
357-
return ((CURLOPT_LASTENTRY%10000) != (310 + 1));
359+
return ((CURLOPT_LASTENTRY%10000) != (311 + 1));
358360
}
359361
#endif

lib/setopt.c

+9
Original file line numberDiff line numberDiff line change
@@ -2477,6 +2477,15 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
24772477
va_arg(param, char *));
24782478
break;
24792479

2480+
case CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256:
2481+
/*
2482+
* Option to allow for the SHA256 of the host public key to be checked
2483+
* for validation purposes.
2484+
*/
2485+
result = Curl_setstropt(&data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256],
2486+
va_arg(param, char *));
2487+
break;
2488+
24802489
case CURLOPT_SSH_KNOWNHOSTS:
24812490
/*
24822491
* Store the file name to read known hosts from.

lib/urldata.h

+1
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,7 @@ enum dupstring {
15541554
STRING_SSH_PRIVATE_KEY, /* path to the private key file for auth */
15551555
STRING_SSH_PUBLIC_KEY, /* path to the public key file for auth */
15561556
STRING_SSH_HOST_PUBLIC_KEY_MD5, /* md5 of host public key in ascii hex */
1557+
STRING_SSH_HOST_PUBLIC_KEY_SHA256, /* sha256 of host public key in base64 */
15571558
STRING_SSH_KNOWNHOSTS, /* file name of knownhosts file */
15581559
STRING_PROXY_SERVICE_NAME, /* Proxy service name */
15591560
STRING_SERVICE_NAME, /* Service name */

lib/vssh/libssh2.c

+130-23
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@
8181
#include "select.h"
8282
#include "warnless.h"
8383
#include "curl_path.h"
84+
#include "strcase.h"
85+
86+
#include <curl_base64.h> /* for base64 encoding/decoding */
87+
#include <curl_sha256.h>
88+
8489

8590
/* The last 3 #include files should be in this order */
8691
#include "curl_printf.h"
@@ -615,40 +620,142 @@ static CURLcode ssh_check_fingerprint(struct Curl_easy *data)
615620
struct connectdata *conn = data->conn;
616621
struct ssh_conn *sshc = &conn->proto.sshc;
617622
const char *pubkey_md5 = data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5];
618-
char md5buffer[33];
623+
const char *pubkey_sha256 = data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256];
624+
625+
infof(data, "SSH MD5 public key: %s",
626+
pubkey_md5 != NULL ? pubkey_md5 : "NULL");
627+
infof(data, "SSH SHA256 public key: %s",
628+
pubkey_sha256 != NULL ? pubkey_sha256 : "NULL");
619629

620-
const char *fingerprint = libssh2_hostkey_hash(sshc->ssh_session,
621-
LIBSSH2_HOSTKEY_HASH_MD5);
630+
if(pubkey_sha256) {
631+
const char *fingerprint = NULL;
632+
char *fingerprint_b64 = NULL;
633+
size_t fingerprint_b64_len;
634+
size_t pub_pos = 0;
635+
size_t b64_pos = 0;
622636

623-
if(fingerprint) {
637+
#ifdef LIBSSH2_HOSTKEY_HASH_SHA256
624638
/* The fingerprint points to static storage (!), don't free() it. */
625-
int i;
626-
for(i = 0; i < 16; i++)
627-
msnprintf(&md5buffer[i*2], 3, "%02x", (unsigned char) fingerprint[i]);
628-
infof(data, "SSH MD5 fingerprint: %s", md5buffer);
629-
}
639+
fingerprint = libssh2_hostkey_hash(sshc->ssh_session,
640+
LIBSSH2_HOSTKEY_HASH_SHA256);
641+
#else
642+
const char *hostkey;
643+
size_t len = 0;
644+
unsigned char hash[32];
645+
646+
hostkey = libssh2_session_hostkey(sshc->ssh_session, &len, NULL);
647+
if(hostkey) {
648+
Curl_sha256it(hash, (const unsigned char *) hostkey, len);
649+
fingerprint = (char *) hash;
650+
}
651+
#endif
630652

631-
/* Before we authenticate we check the hostkey's MD5 fingerprint
632-
* against a known fingerprint, if available.
633-
*/
634-
if(pubkey_md5 && strlen(pubkey_md5) == 32) {
635-
if(!fingerprint || !strcasecompare(md5buffer, pubkey_md5)) {
636-
if(fingerprint)
637-
failf(data,
638-
"Denied establishing ssh session: mismatch md5 fingerprint. "
639-
"Remote %s is not equal to %s", md5buffer, pubkey_md5);
640-
else
641-
failf(data,
642-
"Denied establishing ssh session: md5 fingerprint not available");
653+
if(!fingerprint) {
654+
failf(data,
655+
"Denied establishing ssh session: sha256 fingerprint "
656+
"not available");
657+
state(data, SSH_SESSION_FREE);
658+
sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
659+
return sshc->actualcode;
660+
}
661+
662+
/* The length of fingerprint is 32 bytes for SHA256.
663+
* See libssh2_hostkey_hash documentation. */
664+
if(Curl_base64_encode (data, fingerprint, 32, &fingerprint_b64,
665+
&fingerprint_b64_len) != CURLE_OK) {
666+
state(data, SSH_SESSION_FREE);
667+
sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
668+
return sshc->actualcode;
669+
}
670+
671+
if(!fingerprint_b64) {
672+
failf(data,
673+
"sha256 fingerprint could not be encoded");
674+
state(data, SSH_SESSION_FREE);
675+
sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
676+
return sshc->actualcode;
677+
}
678+
679+
infof(data, "SSH SHA256 fingerprint: %s", fingerprint_b64);
680+
681+
/* Find the position of any = padding characters in the public key */
682+
while((pubkey_sha256[pub_pos] != '=') && pubkey_sha256[pub_pos]) {
683+
pub_pos++;
684+
}
685+
686+
/* Find the position of any = padding characters in the base64 coded
687+
* hostkey fingerprint */
688+
while((fingerprint_b64[b64_pos] != '=') && fingerprint_b64[b64_pos]) {
689+
b64_pos++;
690+
}
691+
692+
/* Before we authenticate we check the hostkey's sha256 fingerprint
693+
* against a known fingerprint, if available.
694+
*/
695+
if((pub_pos != b64_pos) ||
696+
Curl_strncasecompare(fingerprint_b64, pubkey_sha256, pub_pos) != 1) {
697+
free(fingerprint_b64);
698+
699+
failf(data,
700+
"Denied establishing ssh session: mismatch sha256 fingerprint. "
701+
"Remote %s is not equal to %s", fingerprint, pubkey_sha256);
643702
state(data, SSH_SESSION_FREE);
644703
sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
645704
return sshc->actualcode;
646705
}
647-
infof(data, "MD5 checksum match!");
706+
707+
free(fingerprint_b64);
708+
709+
infof(data, "SHA256 checksum match!");
710+
}
711+
712+
if(pubkey_md5) {
713+
char md5buffer[33];
714+
const char *fingerprint = NULL;
715+
716+
fingerprint = libssh2_hostkey_hash(sshc->ssh_session,
717+
LIBSSH2_HOSTKEY_HASH_MD5);
718+
719+
if(fingerprint) {
720+
/* The fingerprint points to static storage (!), don't free() it. */
721+
int i;
722+
for(i = 0; i < 16; i++) {
723+
msnprintf(&md5buffer[i*2], 3, "%02x", (unsigned char) fingerprint[i]);
724+
}
725+
726+
infof(data, "SSH MD5 fingerprint: %s", md5buffer);
727+
}
728+
729+
/* Before we authenticate we check the hostkey's MD5 fingerprint
730+
* against a known fingerprint, if available.
731+
*/
732+
if(pubkey_md5 && strlen(pubkey_md5) == 32) {
733+
if(!fingerprint || !strcasecompare(md5buffer, pubkey_md5)) {
734+
if(fingerprint) {
735+
failf(data,
736+
"Denied establishing ssh session: mismatch md5 fingerprint. "
737+
"Remote %s is not equal to %s", md5buffer, pubkey_md5);
738+
}
739+
else {
740+
failf(data,
741+
"Denied establishing ssh session: md5 fingerprint "
742+
"not available");
743+
}
744+
state(data, SSH_SESSION_FREE);
745+
sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
746+
return sshc->actualcode;
747+
}
748+
infof(data, "MD5 checksum match!");
749+
}
750+
}
751+
752+
if(!pubkey_md5 && !pubkey_sha256) {
753+
return ssh_knownhost(data);
754+
}
755+
else {
648756
/* as we already matched, we skip the check for known hosts */
649757
return CURLE_OK;
650758
}
651-
return ssh_knownhost(data);
652759
}
653760

654761
/*

src/tool_cfgable.c

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ static void free_config_fields(struct OperationConfig *config)
131131
Curl_safefree(config->proxy_key_passwd);
132132
Curl_safefree(config->pubkey);
133133
Curl_safefree(config->hostpubmd5);
134+
Curl_safefree(config->hostpubsha256);
134135
Curl_safefree(config->engine);
135136
Curl_safefree(config->etag_save_file);
136137
Curl_safefree(config->etag_compare_file);

src/tool_cfgable.h

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ struct OperationConfig {
158158
char *proxy_key_passwd;
159159
char *pubkey;
160160
char *hostpubmd5;
161+
char *hostpubsha256;
161162
char *engine;
162163
char *etag_save_file;
163164
char *etag_compare_file;

0 commit comments

Comments
 (0)