Skip to content

Commit

Permalink
libssh2: add SHA256 fingerprint support
Browse files Browse the repository at this point in the history
Added support for SHA256 fingerprint in command line curl and in
libcurl.

Closes curl#7646
  • Loading branch information
Mats Lindestam authored and bagder committed Sep 26, 2021
1 parent 1ca62bb commit d1e7d91
Show file tree
Hide file tree
Showing 27 changed files with 360 additions and 38 deletions.
10 changes: 0 additions & 10 deletions docs/TODO
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/cmdline-opts/Makefile.inc
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ DPAGES = \
header.d \
help.d \
hostpubmd5.d \
hostpubsha256.d \
hsts.d \
http0.9.d \
http1.0.d \
Expand Down
11 changes: 11 additions & 0 deletions docs/cmdline-opts/hostpubsha256.d
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions docs/libcurl/curl_easy_setopt.3
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions docs/libcurl/opts/CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256.3
Original file line number Diff line number Diff line change
@@ -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), "
1 change: 1 addition & 0 deletions docs/libcurl/opts/Makefile.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
1 change: 1 addition & 0 deletions docs/libcurl/symbols-in-versions
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/options-in-versions
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions include/curl/curl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions include/curl/typecheck-gcc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 || \
Expand Down
4 changes: 3 additions & 1 deletion lib/easyoptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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
9 changes: 9 additions & 0 deletions lib/setopt.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions lib/urldata.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
153 changes: 130 additions & 23 deletions lib/vssh/libssh2.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);
}

/*
Expand Down
1 change: 1 addition & 0 deletions src/tool_cfgable.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/tool_cfgable.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit d1e7d91

Please sign in to comment.