diff --git a/meson.build b/meson.build index 20bf84e013..00a83b5f90 100644 --- a/meson.build +++ b/meson.build @@ -30,6 +30,10 @@ cc = meson.get_compiler('c') # OS-specific settings #################################################################################################################################### if host_machine.system() == 'linux' + # BSD_SOURCE can be removed when support for RHEL7 is dropped, + # DEFAULT_SOURCE can be removed when support for RHEL7 is dropped if we rewrite a few hstrerror calls to a generic error message + add_global_arguments('-D_DEFAULT_SOURCE', language : 'c') + add_global_arguments('-D_BSD_SOURCE', language : 'c') add_global_arguments('-D_POSIX_C_SOURCE=200809L', language : 'c') elif host_machine.system() == 'darwin' add_global_arguments('-D_DARWIN_C_SOURCE', language : 'c') @@ -174,6 +178,13 @@ if lib_ssh2.found() configuration.set('HAVE_LIBSSH2', true, description: 'Is libssh2 present?') endif +# Find optional libresolv library +lib_resolv = cc.find_library('resolv', required: false) + +if lib_resolv.found() + configuration.set('HAVE_LIBRESOLV', true, description: 'Is libresolv present?') +endif + # Find optional zstd library lib_zstd = dependency('libzstd', version: '>=1.0', required: false) diff --git a/src/build/config/config.yaml b/src/build/config/config.yaml index a8ff09b30f..29a990b513 100644 --- a/src/build/config/config.yaml +++ b/src/build/config/config.yaml @@ -2460,6 +2460,14 @@ option: command: repo-type depend: repo-sftp-host + repo-sftp-verify-via-sshfp: + section: global + group: repo + type: boolean + default: false + command: repo-type + depend: repo-sftp-host + repo-storage-verify-tls: section: global group: repo diff --git a/src/build/configure.ac b/src/build/configure.ac index 78e03d7bd5..2d2638f0c8 100644 --- a/src/build/configure.ac +++ b/src/build/configure.ac @@ -15,12 +15,17 @@ AC_PROG_CC AC_CANONICAL_HOST AC_SUBST(CFLAGS, "${CFLAGS} -std=c99") +# BSD_SOURCE can be removed when support for RHEL7 is dropped +# DEFAULT_SOURCE can be removed when support for RHEL7 is dropped if we rewrite a few hstrerror calls to a generic error message +# ---------------------------------------------------------------------------------------------------------------------------------- case $host_os in darwin*) AC_SUBST(CPPFLAGS, "${CPPFLAGS} -D_DARWIN_C_SOURCE") ;; linux*) + AC_SUBST(CPPFLAGS, "${CPPFLAGS} -D_DEFAULT_SOURCE") + AC_SUBST(CPPFLAGS, "${CPPFLAGS} -D_BSD_SOURCE") AC_SUBST(CPPFLAGS, "${CPPFLAGS} -D_POSIX_C_SOURCE=200809L") ;; esac @@ -113,7 +118,7 @@ AC_CHECK_LIB( [AC_CHECK_HEADER(lz4frame.h, [AC_DEFINE(HAVE_LIBLZ4) AC_SUBST(LIBS, "${LIBS} -llz4")], [AC_MSG_ERROR([header file is required])])]) -# Check optional libSSH2 library +# Check optional libssh2 library # ---------------------------------------------------------------------------------------------------------------------------------- AC_CHECK_LIB( [ssh2], [libssh2_init], @@ -124,6 +129,14 @@ AC_CHECK_LIB( [AC_CHECK_HEADER(libssh2_sftp.h, [], [AC_MSG_ERROR([header file is required])])]) +# Check for optional resolv library if we have libssh2 +# ---------------------------------------------------------------------------------------------------------------------------------- +if test "x$ac_cv_lib_ssh2_libssh2_init" = "xyes" +then + AC_CHECK_LIB([resolv], [ns_initparse], [], []) + AC_HEADER_RESOLV +fi + # Check optional zst library. Ignore any versions below 1.0. # ---------------------------------------------------------------------------------------------------------------------------------- AC_CHECK_LIB( diff --git a/src/build/help/help.xml b/src/build/help/help.xml index 8b9c94d7a5..df92de5097 100644 --- a/src/build/help/help.xml +++ b/src/build/help/help.xml @@ -1059,6 +1059,17 @@ path + + SFTP verify via SSHFP. + + +

Perform fingerprint verification via SSHFP records. This assumes that DNS resolution is configured to resolve to a DNS server that has been properly configured for DNSSEC and that the path between the host and the DNS server is secure. The OS must support RES_TRUSTAD (ad flag) in order to verify via SSHFP. If any DNS provided fingerprints match, the host will be trusted. If the DNS response ad flag is not set, or no DNS fingerprints are provided, or no DNS fingerprints match the host fingerprint, warnings are logged and will failover to attempt to verify via normal methods.

+
+ + n + y +
+ SFTP repository host. diff --git a/src/config/config.auto.h b/src/config/config.auto.h index cb9204c01f..dbaac10a6a 100644 --- a/src/config/config.auto.h +++ b/src/config/config.auto.h @@ -136,7 +136,7 @@ Option constants #define CFGOPT_TYPE "type" #define CFGOPT_VERBOSE "verbose" -#define CFG_OPTION_TOTAL 179 +#define CFG_OPTION_TOTAL 180 /*********************************************************************************************************************************** Option value constants @@ -528,6 +528,7 @@ typedef enum cfgOptRepoSftpPrivateKeyFile, cfgOptRepoSftpPrivateKeyPassphrase, cfgOptRepoSftpPublicKeyFile, + cfgOptRepoSftpVerifyViaSshfp, cfgOptRepoStorageCaFile, cfgOptRepoStorageCaPath, cfgOptRepoStorageHost, diff --git a/src/config/parse.auto.c.inc b/src/config/parse.auto.c.inc index 609baa1c24..daf458d73c 100644 --- a/src/config/parse.auto.c.inc +++ b/src/config/parse.auto.c.inc @@ -8733,6 +8733,92 @@ static const ParseRuleOption parseRuleOption[CFG_OPTION_TOTAL] = ), // opt/repo-sftp-public-key-file ), // opt/repo-sftp-public-key-file // ----------------------------------------------------------------------------------------------------------------------------- + PARSE_RULE_OPTION // opt/repo-sftp-verify-via-sshfp + ( // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_NAME("repo-sftp-verify-via-sshfp"), // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_TYPE(cfgOptTypeBoolean), // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_NEGATE(true), // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_RESET(true), // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_REQUIRED(true), // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_SECTION(cfgSectionGlobal), // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_GROUP_MEMBER(true), // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_GROUP_ID(cfgOptGrpRepo), // opt/repo-sftp-verify-via-sshfp + // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND_ROLE_MAIN_VALID_LIST // opt/repo-sftp-verify-via-sshfp + ( // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdExpire) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-verify-via-sshfp + ), // opt/repo-sftp-verify-via-sshfp + // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND_ROLE_ASYNC_VALID_LIST // opt/repo-sftp-verify-via-sshfp + ( // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-verify-via-sshfp + ), // opt/repo-sftp-verify-via-sshfp + // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND_ROLE_LOCAL_VALID_LIST // opt/repo-sftp-verify-via-sshfp + ( // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-verify-via-sshfp + ), // opt/repo-sftp-verify-via-sshfp + // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND_ROLE_REMOTE_VALID_LIST // opt/repo-sftp-verify-via-sshfp + ( // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-verify-via-sshfp + ), // opt/repo-sftp-verify-via-sshfp + // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTIONAL // opt/repo-sftp-verify-via-sshfp + ( // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTIONAL_GROUP // opt/repo-sftp-verify-via-sshfp + ( // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTIONAL_DEPEND // opt/repo-sftp-verify-via-sshfp + ( // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_VAL_OPT(cfgOptRepoType), // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_VAL_STRID(parseRuleValStrIdSftp), // opt/repo-sftp-verify-via-sshfp + ), // opt/repo-sftp-verify-via-sshfp + // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_OPTIONAL_DEFAULT // opt/repo-sftp-verify-via-sshfp + ( // opt/repo-sftp-verify-via-sshfp + PARSE_RULE_VAL_BOOL_FALSE, // opt/repo-sftp-verify-via-sshfp + ), // opt/repo-sftp-verify-via-sshfp + ), // opt/repo-sftp-verify-via-sshfp + ), // opt/repo-sftp-verify-via-sshfp + ), // opt/repo-sftp-verify-via-sshfp + // ----------------------------------------------------------------------------------------------------------------------------- PARSE_RULE_OPTION // opt/repo-storage-ca-file ( // opt/repo-storage-ca-file PARSE_RULE_OPTION_NAME("repo-storage-ca-file"), // opt/repo-storage-ca-file @@ -10988,6 +11074,7 @@ static const uint8_t optionResolveOrder[] = cfgOptRepoSftpPrivateKeyFile, // opt-resolve-order cfgOptRepoSftpPrivateKeyPassphrase, // opt-resolve-order cfgOptRepoSftpPublicKeyFile, // opt-resolve-order + cfgOptRepoSftpVerifyViaSshfp, // opt-resolve-order cfgOptRepoStorageCaFile, // opt-resolve-order cfgOptRepoStorageCaPath, // opt-resolve-order cfgOptRepoStorageHost, // opt-resolve-order diff --git a/src/configure b/src/configure index 958124800e..154b8625bc 100755 --- a/src/configure +++ b/src/configure @@ -3408,6 +3408,9 @@ case $host_os in *\ *) host_os=`echo "$host_os" | sed 's/ /-/g'`;; esac CFLAGS="${CFLAGS} -std=c99" +# BSD_SOURCE can be removed when support for RHEL7 is dropped +# DEFAULT_SOURCE can be removed when support for RHEL7 is dropped if we rewrite a few hstrerror calls to a generic error message +# ---------------------------------------------------------------------------------------------------------------------------------- case $host_os in darwin*) CPPFLAGS="${CPPFLAGS} -D_DARWIN_C_SOURCE" @@ -3415,6 +3418,10 @@ case $host_os in ;; linux*) + CPPFLAGS="${CPPFLAGS} -D_DEFAULT_SOURCE" + + CPPFLAGS="${CPPFLAGS} -D_BSD_SOURCE" + CPPFLAGS="${CPPFLAGS} -D_POSIX_C_SOURCE=200809L" ;; @@ -4086,7 +4093,7 @@ fi fi -# Check optional libSSH2 library +# Check optional libssh2 library # ---------------------------------------------------------------------------------------------------------------------------------- { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libssh2_init in -lssh2" >&5 printf %s "checking for libssh2_init in -lssh2... " >&6; } @@ -4185,6 +4192,147 @@ fi fi +# Check for optional resolv library if we have libssh2 +# ---------------------------------------------------------------------------------------------------------------------------------- +if test "x$ac_cv_lib_ssh2_libssh2_init" = "xyes" +then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for ns_initparse in -lresolv" >&5 +printf %s "checking for ns_initparse in -lresolv... " >&6; } +if test ${ac_cv_lib_resolv_ns_initparse+y} +then : + printf %s "(cached) " >&6 +else $as_nop + ac_check_lib_save_LIBS=$LIBS +LIBS="-lresolv $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +char ns_initparse (); +int +main (void) +{ +return ns_initparse (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO" +then : + ac_cv_lib_resolv_ns_initparse=yes +else $as_nop + ac_cv_lib_resolv_ns_initparse=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_resolv_ns_initparse" >&5 +printf "%s\n" "$ac_cv_lib_resolv_ns_initparse" >&6; } +if test "x$ac_cv_lib_resolv_ns_initparse" = xyes +then : + printf "%s\n" "#define HAVE_LIBRESOLV 1" >>confdefs.h + + LIBS="-lresolv $LIBS" + +fi + + ac_fn_c_check_header_compile "$LINENO" "sys/types.h" "ac_cv_header_sys_types_h" "#ifdef HAVE_SYS_TYPES_H +# include +#endif +#ifdef HAVE_NETINET_IN_H +# include /* inet_ functions / structs */ +#endif +#ifdef HAVE_ARPA_NAMESER_H +# include /* DNS HEADER struct */ +#endif +#ifdef HAVE_NETDB_H +# include +#endif +" +if test "x$ac_cv_header_sys_types_h" = xyes +then : + printf "%s\n" "#define HAVE_SYS_TYPES_H 1" >>confdefs.h + +fi +ac_fn_c_check_header_compile "$LINENO" "netinet/in.h" "ac_cv_header_netinet_in_h" "#ifdef HAVE_SYS_TYPES_H +# include +#endif +#ifdef HAVE_NETINET_IN_H +# include /* inet_ functions / structs */ +#endif +#ifdef HAVE_ARPA_NAMESER_H +# include /* DNS HEADER struct */ +#endif +#ifdef HAVE_NETDB_H +# include +#endif +" +if test "x$ac_cv_header_netinet_in_h" = xyes +then : + printf "%s\n" "#define HAVE_NETINET_IN_H 1" >>confdefs.h + +fi +ac_fn_c_check_header_compile "$LINENO" "arpa/nameser.h" "ac_cv_header_arpa_nameser_h" "#ifdef HAVE_SYS_TYPES_H +# include +#endif +#ifdef HAVE_NETINET_IN_H +# include /* inet_ functions / structs */ +#endif +#ifdef HAVE_ARPA_NAMESER_H +# include /* DNS HEADER struct */ +#endif +#ifdef HAVE_NETDB_H +# include +#endif +" +if test "x$ac_cv_header_arpa_nameser_h" = xyes +then : + printf "%s\n" "#define HAVE_ARPA_NAMESER_H 1" >>confdefs.h + +fi +ac_fn_c_check_header_compile "$LINENO" "netdb.h" "ac_cv_header_netdb_h" "#ifdef HAVE_SYS_TYPES_H +# include +#endif +#ifdef HAVE_NETINET_IN_H +# include /* inet_ functions / structs */ +#endif +#ifdef HAVE_ARPA_NAMESER_H +# include /* DNS HEADER struct */ +#endif +#ifdef HAVE_NETDB_H +# include +#endif +" +if test "x$ac_cv_header_netdb_h" = xyes +then : + printf "%s\n" "#define HAVE_NETDB_H 1" >>confdefs.h + +fi +ac_fn_c_check_header_compile "$LINENO" "resolv.h" "ac_cv_header_resolv_h" "#ifdef HAVE_SYS_TYPES_H +# include +#endif +#ifdef HAVE_NETINET_IN_H +# include /* inet_ functions / structs */ +#endif +#ifdef HAVE_ARPA_NAMESER_H +# include /* DNS HEADER struct */ +#endif +#ifdef HAVE_NETDB_H +# include +#endif +" +if test "x$ac_cv_header_resolv_h" = xyes +then : + printf "%s\n" "#define HAVE_RESOLV_H 1" >>confdefs.h + +fi + + +fi + # Check optional zst library. Ignore any versions below 1.0. # ---------------------------------------------------------------------------------------------------------------------------------- { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for ZSTD_isError in -lzstd" >&5 @@ -5750,4 +5898,4 @@ if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} fi -# Generated from src/build/configure.ac sha1 e9fe59c2eda5217a0f6476af90083fafb8b24f94 +# Generated from src/build/configure.ac sha1 d02753966f72fc0ea5ee493613a5b2aac0852b5c diff --git a/src/meson.build b/src/meson.build index 779e510a3b..3f8194eaa6 100644 --- a/src/meson.build +++ b/src/meson.build @@ -277,6 +277,7 @@ executable( lib_lz4, lib_pq, lib_ssh2, + lib_resolv, lib_xml, lib_z, lib_zstd, diff --git a/src/storage/sftp/helper.c b/src/storage/sftp/helper.c index 7a1321a226..ef7524efc5 100644 --- a/src/storage/sftp/helper.c +++ b/src/storage/sftp/helper.c @@ -39,7 +39,8 @@ storageSftpHelper(const unsigned int repoIdx, const bool write, StoragePathExpre .modePath = STORAGE_MODE_PATH_DEFAULT, .keyPub = cfgOptionIdxStrNull(cfgOptRepoSftpPublicKeyFile, repoIdx), .keyPassphrase = cfgOptionIdxStrNull(cfgOptRepoSftpPrivateKeyPassphrase, repoIdx), .hostKeyCheckType = cfgOptionIdxStrId(cfgOptRepoSftpHostKeyCheckType, repoIdx), - .hostFingerprint = cfgOptionIdxStrNull(cfgOptRepoSftpHostFingerprint, repoIdx), .knownHosts = knownHosts); + .hostFingerprint = cfgOptionIdxStrNull(cfgOptRepoSftpHostFingerprint, repoIdx), + .sshfp = cfgOptionIdxBool(cfgOptRepoSftpVerifyViaSshfp, repoIdx), .knownHosts = knownHosts); } MEM_CONTEXT_PRIOR_END(); } diff --git a/src/storage/sftp/storage.c b/src/storage/sftp/storage.c index c592c208e3..1e4b2f070d 100644 --- a/src/storage/sftp/storage.c +++ b/src/storage/sftp/storage.c @@ -5,6 +5,10 @@ SFTP Storage #ifdef HAVE_LIBSSH2 +#include +#include +#include + #include "common/crypto/hash.h" #include "common/debug.h" #include "common/io/fd.h" @@ -24,6 +28,13 @@ Define PATH_MAX if it is not defined #define PATH_MAX (4 * 1024) #endif +/*********************************************************************************************************************************** +Define PACKET_SZ if it is not defined +***********************************************************************************************************************************/ +#ifndef PACKET_SZ +#define PACKET_SZ 65535 +#endif + /*********************************************************************************************************************************** Object type ***********************************************************************************************************************************/ @@ -38,6 +49,9 @@ struct StorageSftp TimeMSec timeout; // Session timeout }; +// Initialize the resolver +struct __res_state my_res_state = {0}; + /*********************************************************************************************************************************** Return known host key type based on host key type ***********************************************************************************************************************************/ @@ -1064,6 +1078,216 @@ storageSftpPathRemove(THIS_VOID, const String *const path, const bool recurse, c FUNCTION_LOG_RETURN(BOOL, result); } +/**********************************************************************************************************************************/ +static int +storageSftpResNinit(res_state statep) +{ + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(VOID, statep); + FUNCTION_LOG_END(); + + ASSERT(statep != NULL); + + FUNCTION_LOG_RETURN(INT, res_ninit(statep)); +} + +/**********************************************************************************************************************************/ +static int +storageSftpResNquery( + res_state statep, const char *const dname, const int class, const int type, unsigned char *answer, + const int anslen) +{ + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(VOID, statep); + FUNCTION_LOG_PARAM(STRINGZ, dname); + FUNCTION_LOG_PARAM(INT, class); + FUNCTION_LOG_PARAM(INT, type); + FUNCTION_LOG_PARAM_P(UCHARDATA, answer); + FUNCTION_LOG_PARAM(INT, anslen); + FUNCTION_LOG_END(); + + ASSERT(statep != NULL); + ASSERT(dname != NULL); + ASSERT(class >= ns_c_invalid && class <= ns_c_max); + ASSERT(type >= ns_t_invalid && type <= ns_t_max); + + FUNCTION_LOG_RETURN(INT, res_nquery(statep, dname, class, type, answer, anslen)); +} + +/**********************************************************************************************************************************/ +static int +storageSftpNsInitparse(const unsigned char *answer, int len, ns_msg *handle) +{ + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM_P(UCHARDATA, answer); + FUNCTION_LOG_PARAM(INT, len); + FUNCTION_LOG_PARAM_P(VOID, handle); + FUNCTION_LOG_END(); + + FUNCTION_LOG_RETURN(INT, ns_initparse(answer, len, handle)); +} + +/**********************************************************************************************************************************/ +#ifdef RES_TRUSTAD +static void +storageSftpSetOption(res_state statep, const uint32_t option) +{ + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(VOID, statep); + FUNCTION_LOG_PARAM(UINT32, option); + FUNCTION_LOG_END(); + + ASSERT(statep != NULL); + ASSERT(option >= RES_INIT && option <= RES_TRUSTAD); + + statep->options |= option; + + FUNCTION_LOG_RETURN_VOID(); +} + +#endif // RES_TRUSTAD + +/**********************************************************************************************************************************/ +static bool +storageSftpVerifyFingerprint(LIBSSH2_SESSION *const session, ns_msg handle) +{ + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM_P(VOID, session); + FUNCTION_LOG_PARAM(VOID, handle); + FUNCTION_LOG_END(); + + bool result = false; + bool foundSshfpRecord = false; + + // Check the sshfp resource records for a fingerprint match + for (int rrnum = 0; rrnum < ns_msg_count(handle, ns_s_an); rrnum++) + { + ns_rr rr; + + // Parse the resource record + ns_parserr(&handle, ns_s_an, rrnum, &rr); + + // Skip non-sshfp records + if (ns_rr_type(rr) != ns_t_sshfp) + continue; + + foundSshfpRecord = true; + + const uint8_t digest_type = *((unsigned char *) ns_rr_rdata(rr) + 1); + const unsigned char *digest = (unsigned char *) ns_rr_rdata(rr) + 2; + + // Only SHA1 and SHA256 are currently defined as valid SSHFP RR types for fingerprint types, default to sha1 + int hashType = LIBSSH2_HOSTKEY_HASH_SHA1; + size_t hashSize = HASH_TYPE_SHA1_SIZE; + + // Newer versions of libssh2 support SHA256, check for it +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + if (digest_type == 2) + { + hashType = LIBSSH2_HOSTKEY_HASH_SHA256; + hashSize = HASH_TYPE_SHA256_SIZE; + } +#endif // LIBSSH2_HOSTKEY_HASH_SHA256 + + // Generate hex encoded sshfp digest + char buffer[256]; + encodeToStr(encodingHex, digest, hashSize, buffer); + + // Compare the fingerprints + const char *binaryFingerprint = libssh2_hostkey_hash(session, hashType); + + if (binaryFingerprint != NULL && memcmp(binaryFingerprint, digest, (size_t)ns_rr_rdlen(rr) - 2) == 0) + { + result = true; + + LOG_DETAIL_FMT( + "sshfp fingerprint match found for sshfp digest_type [%d] hashType [%d] '%s'", digest_type, hashType, buffer); + } + else + LOG_DETAIL_FMT( + "no sshfp fingerprint match found for sshfp digest_type [%d] hashType [%d] '%s'", digest_type, hashType, buffer); + } + + if (!foundSshfpRecord) + LOG_WARN("no SSHFP records for host found in DNS"); + + FUNCTION_LOG_RETURN(BOOL, result); +} + +/*********************************************************************************************************************************** +Perform minimal DNS verification on the host. Queries with RES_TRUSTAD if supported and verifies that response is RES_TRUSTAD. +Checks that the hostkey is returned in the SSHFP list. It is predicated on the fact that the DNS server is properly configured for +DNSSEC and the communication path between the host and the DNS server is secure. +***********************************************************************************************************************************/ +static bool +storageSftpSshfp(StorageSftp *const this, const String *const host) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(STORAGE_SFTP, this); + FUNCTION_LOG_PARAM(STRING, host); + FUNCTION_LOG_END(); + + ASSERT(host != NULL); + + bool result = false; + + if (storageSftpResNinit(&my_res_state) != 0) + LOG_WARN("unable to initialize resolver"); + +#ifdef RES_TRUSTAD + // Set the resolver to use TRUSTAD + storageSftpSetOption(&my_res_state, RES_TRUSTAD); +#endif // RES_TRUSTAD + + // Query the server for SSHFP records + unsigned char answer[PACKET_SZ]; + + int len = storageSftpResNquery(&my_res_state, strZ(host), C_IN, ns_t_sshfp, answer, sizeof(answer)); + + // Check for errors. Error msg is dependent on keeping the _DEFAULT_SOURCE for netdb.h. We can drop it and rewrite to a generic + // error if we think that's better. + if (len < 0) + LOG_WARN_FMT("res_nquery error [%d] %s '%s'", my_res_state.res_h_errno, hstrerror(my_res_state.res_h_errno), strZ(host)); + + // Default res_trustad to unset + unsigned char res_trustad = 0; + +#ifdef RES_TRUSTAD + // Check the RES_TRUSTAD flag + res_trustad = ((HEADER *)answer)->ad; +#endif // RES_TRUSTAD + + if (res_trustad != 1) + LOG_WARN("Host cannot be verified via SSHFP, RES_TRUSTAD not set in response"); + +#ifndef RES_TRUSTAD + LOG_WARN_FMT("RES_TRUSTAD not supported on this OS, host '%s' cannot be verified via SSHFP", strZ(host)); +#endif // RES_TRUSTAD + + // If res_trustad is not set, we still check for sshfp records, we can't verify them, but we may be able to provide useful + // information to the user. + + // Initialize parsing the response + int rc; + ns_msg handle; + if ((rc = storageSftpNsInitparse(answer, len, &handle)) != 0) + LOG_WARN_FMT("ns_initparse error [%d] %s for host '%s'", rc, hstrerror(rc), strZ(host)); + + // Attempt to verify the host via DNS provided fingerprint + const bool sshfpVerified = storageSftpVerifyFingerprint(this->session, handle); + + // Close the resolver + res_nclose(&my_res_state); + + // If res_trustad is not set, return false, else return the result of the fingerprint verification + if (res_trustad == 0) + result = false; + else + result = sshfpVerified; + + FUNCTION_LOG_RETURN(BOOL, result); +} + /**********************************************************************************************************************************/ static const StorageInterface storageInterfaceSftp = { @@ -1098,6 +1322,7 @@ storageSftpNew( FUNCTION_LOG_PARAM(STRING_LIST, param.knownHosts); FUNCTION_LOG_PARAM(MODE, param.modeFile); FUNCTION_LOG_PARAM(MODE, param.modePath); + FUNCTION_LOG_PARAM(BOOL, param.sshfp); FUNCTION_LOG_PARAM(BOOL, param.write); FUNCTION_LOG_PARAM(FUNCTIONP, param.pathExpressionFunction); FUNCTION_LOG_END(); @@ -1179,171 +1404,183 @@ storageSftpNew( break; } - // Compare fingerprint if provided else check known hosts files for a match - if (param.hostKeyCheckType == SFTP_STRICT_HOSTKEY_CHECKING_FINGERPRINT) - { - const char *const binaryFingerprint = libssh2_hostkey_hash(this->session, hashType); + // Attempt to verify via SSHFP fingerprint if requested. If verifiedViaSshfp is successful we implicitly trust the host. + bool verifiedViaSshfp = false; + if (param.sshfp) + verifiedViaSshfp = storageSftpSshfp(this, host); - if (binaryFingerprint == NULL) - { - THROW_FMT( - ServiceError, "libssh2 hostkey hash failed: libssh2 errno [%d]", libssh2_session_last_errno(this->session)); - } - - // 256 bytes is large enough to hold the hex representation of currently supported hash types. The hex encoded version - // requires twice as much space (hashSize * 2) as the raw version. - char fingerprint[256]; - - encodeToStr(encodingHex, (unsigned char *)binaryFingerprint, hashSize, fingerprint); - - if (strcmp(fingerprint, strZ(param.hostFingerprint)) != 0) - { - THROW_FMT( - ServiceError, "host [%s] and configured fingerprint (repo-sftp-host-fingerprint) [%s] do not match", - fingerprint, strZ(param.hostFingerprint)); - } - } - else if (param.hostKeyCheckType != SFTP_STRICT_HOSTKEY_CHECKING_NONE) + // If we did not verify via SSHFP then we need to verify via normal means + if (!verifiedViaSshfp) { - // Init the known host collection - LIBSSH2_KNOWNHOSTS *const knownHostsList = libssh2_knownhost_init(this->session); - - if (knownHostsList == NULL) + // Compare fingerprint if provided else check known hosts files for a match + if (param.hostKeyCheckType == SFTP_STRICT_HOSTKEY_CHECKING_FINGERPRINT) { - const int rc = libssh2_session_last_errno(this->session); + const char *const binaryFingerprint = libssh2_hostkey_hash(this->session, hashType); - THROW_FMT( - ServiceError, - "failure during libssh2_knownhost_init: libssh2 errno [%d] %s", rc, - strZ(storageSftpLibSsh2SessionLastError(this->session))); - } + if (binaryFingerprint == NULL) + { + THROW_FMT( + ServiceError, "libssh2 hostkey hash failed: libssh2 errno [%d]", libssh2_session_last_errno(this->session)); + } - // Get the list of known host files to search - const StringList *const knownHostsPathList = storageSftpKnownHostsFilesList(param.knownHosts); + // 256 bytes is large enough to hold the hex representation of currently supported hash types. The hex encoded + // version requires twice as much space (hashSize * 2) as the raw version. + char fingerprint[256]; - // Loop through the list of known host files - for (unsigned int listIdx = 0; listIdx < strLstSize(knownHostsPathList); listIdx++) - { - const char *const currentKnownHostFile = strZNull(strLstGet(knownHostsPathList, listIdx)); + encodeToStr(encodingHex, (unsigned char *)binaryFingerprint, hashSize, fingerprint); - // Read the known hosts file entries into the collection, log message for readfile status. - // libssh2_knownhost_readfile() returns the number of successfully loaded hosts or a negative value on error, an - // empty known hosts file will return 0. - if ((rc = libssh2_knownhost_readfile(knownHostsList, currentKnownHostFile, LIBSSH2_KNOWNHOST_FILE_OPENSSH)) <= 0) + if (strcmp(fingerprint, strZ(param.hostFingerprint)) != 0) { - if (rc == 0) - LOG_DETAIL_FMT("libssh2 '%s' file is empty", currentKnownHostFile); - else - { - LOG_DETAIL_FMT( - "libssh2 read '%s' failed: libssh2 errno [%d] %s", currentKnownHostFile, rc, - strZ(storageSftpLibSsh2SessionLastError(this->session))); - } + THROW_FMT( + ServiceError, "host [%s] and configured fingerprint (repo-sftp-host-fingerprint) [%s] do not match", + fingerprint, strZ(param.hostFingerprint)); } - else - LOG_DETAIL_FMT("libssh2 read '%s' succeeded", currentKnownHostFile); } + else if (param.hostKeyCheckType != SFTP_STRICT_HOSTKEY_CHECKING_NONE) + { + // Init the known host collection + LIBSSH2_KNOWNHOSTS *const knownHostsList = libssh2_knownhost_init(this->session); - // Get the remote host key - size_t hostKeyLen; - int hostKeyType; - const char *const hostKey = libssh2_session_hostkey(this->session, &hostKeyLen, &hostKeyType); + if (knownHostsList == NULL) + { + const int rc = libssh2_session_last_errno(this->session); - // Check for a match in known hosts files else throw an error if no host key was retrieved - if (hostKey != NULL) - { - rc = libssh2_knownhost_checkp( - knownHostsList, strZ(host), (int)port, hostKey, hostKeyLen, - LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW, NULL); + THROW_FMT( + ServiceError, "failure during libssh2_knownhost_init: libssh2 errno [%d] %s", rc, + strZ(storageSftpLibSsh2SessionLastError(this->session))); + } - // Handle check success/failure - if (rc == LIBSSH2_KNOWNHOST_CHECK_MATCH) - LOG_DETAIL_FMT("known hosts match found for '%s'", strZ(host)); - else + // Get the list of known host files to search + const StringList *const knownHostsPathList = storageSftpKnownHostsFilesList(param.knownHosts); + + // Loop through the list of known host files + for (unsigned int listIdx = 0; listIdx < strLstSize(knownHostsPathList); listIdx++) { - // Handle failure to match in a similar manner as ssh_config StrictHostKeyChecking. If this flag is set to - // "strict", never automatically add host keys to the ~/.ssh/known_hosts file, and refuse to connect to hosts - // whose host key has changed. This option forces the user to manually add all new hosts. If this flag is set to - // "accept-new" then automatically add new host keys to the user known hosts files, but do not permit - // connections to hosts with changed host keys. - switch (param.hostKeyCheckType) + const char *const currentKnownHostFile = strZNull(strLstGet(knownHostsPathList, listIdx)); + + // Read the known hosts file entries into the collection, log message for readfile status. + // libssh2_knownhost_readfile() returns the number of successfully loaded hosts or a negative value on error, an + // empty known hosts file will return 0. + if ((rc = libssh2_knownhost_readfile( + knownHostsList, currentKnownHostFile, LIBSSH2_KNOWNHOST_FILE_OPENSSH)) <= 0) { - case SFTP_STRICT_HOSTKEY_CHECKING_STRICT: + if (rc == 0) + LOG_DETAIL_FMT("libssh2 '%s' file is empty", currentKnownHostFile); + else { - // Throw an error when set to strict and we have any result other than match - libssh2_knownhost_free(knownHostsList); - - THROW_FMT( - ServiceError, "known hosts failure: '%s' %s [%d]: check type [%s]", strZ(host), - storageSftpKnownHostCheckpFailureMsg(rc), rc, strZ(strIdToStr(param.hostKeyCheckType))); + const String *const libssh2ErrMsg = storageSftpLibSsh2SessionLastError(this->session); - break; + LOG_DETAIL_FMT( + "libssh2 read '%s' failed: libssh2 errno [%d] %s", currentKnownHostFile, rc, strZ(libssh2ErrMsg)); } + } + else + LOG_DETAIL_FMT("libssh2 read '%s' succeeded", currentKnownHostFile); + } - default: - { - ASSERT(param.hostKeyCheckType == SFTP_STRICT_HOSTKEY_CHECKING_ACCEPT_NEW); + // Get the remote host key + size_t hostKeyLen; + int hostKeyType; + const char *const hostKey = libssh2_session_hostkey(this->session, &hostKeyLen, &hostKeyType); + + // Check for a match in known hosts files else throw an error if no host key was retrieved + if (hostKey != NULL) + { + rc = libssh2_knownhost_checkp( + knownHostsList, strZ(host), (int)port, hostKey, hostKeyLen, + LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW, NULL); - // Throw an error when set to accept-new and match fails or mismatches else add the new host key to the - // user's known_hosts file - if (rc == LIBSSH2_KNOWNHOST_CHECK_MISMATCH || rc == LIBSSH2_KNOWNHOST_CHECK_FAILURE) + // Handle check success/failure + if (rc == LIBSSH2_KNOWNHOST_CHECK_MATCH) + LOG_DETAIL_FMT("known hosts match found for '%s'", strZ(host)); + else + { + // Handle failure to match in a similar manner as ssh_config StrictHostKeyChecking. If this flag is set to + // "strict", never automatically add host keys to the ~/.ssh/known_hosts file, and refuse to connect to + // hosts whose host key has changed. This option forces the user to manually add all new hosts. If this flag + // is set to "accept-new" then automatically add new host keys to the user known hosts files, but do not + // permit connections to hosts with changed host keys. + switch (param.hostKeyCheckType) + { + case SFTP_STRICT_HOSTKEY_CHECKING_STRICT: { - // Free the known hosts list + // Throw an error when set to strict and we have any result other than match libssh2_knownhost_free(knownHostsList); THROW_FMT( - ServiceError, "known hosts failure: '%s': %s [%d]: check type [%s]", strZ(host), - storageSftpKnownHostCheckpFailureMsg(rc), rc, - strZ(strIdToStr(param.hostKeyCheckType))); + ServiceError, "known hosts failure: '%s' %s [%d]: check type [%s]", strZ(host), + storageSftpKnownHostCheckpFailureMsg(rc), rc, strZ(strIdToStr(param.hostKeyCheckType))); + + break; } - else - storageSftpUpdateKnownHostsFile(this, hostKeyType, host, hostKey, hostKeyLen); - break; + default: + { + ASSERT(param.hostKeyCheckType == SFTP_STRICT_HOSTKEY_CHECKING_ACCEPT_NEW); + + // Throw an error when set to accept-new and match fails or mismatches else add the new host key to + // the user's known_hosts file. + if (rc == LIBSSH2_KNOWNHOST_CHECK_MISMATCH || rc == LIBSSH2_KNOWNHOST_CHECK_FAILURE) + { + // Free the known hosts list + libssh2_knownhost_free(knownHostsList); + + THROW_FMT( + ServiceError, "known hosts failure: '%s': %s [%d]: check type [%s]", strZ(host), + storageSftpKnownHostCheckpFailureMsg(rc), rc, + strZ(strIdToStr(param.hostKeyCheckType))); + } + else + storageSftpUpdateKnownHostsFile(this, hostKeyType, host, hostKey, hostKeyLen); + + break; + } } } } - } - else - { - THROW_FMT( - ServiceError, - "libssh2_session_hostkey failed to get hostkey: libssh2 error [%d]", libssh2_session_last_errno(this->session)); - } + else + { + THROW_FMT( + ServiceError, + "libssh2_session_hostkey failed to get hostkey: libssh2 error [%d]", + libssh2_session_last_errno(this->session)); + } - // Free the known hosts list - libssh2_knownhost_free(knownHostsList); - } + // Free the known hosts list + libssh2_knownhost_free(knownHostsList); + } - // Perform public key authorization, expand leading tilde key file paths if needed - String *const privKeyPath = regExpMatchOne(STRDEF("^ *~"), keyPriv) ? storageSftpExpandTildePath(keyPriv) : strDup(keyPriv); - String *const pubKeyPath = - param.keyPub != NULL && regExpMatchOne(STRDEF("^ *~"), param.keyPub) ? - storageSftpExpandTildePath(param.keyPub) : strDup(param.keyPub); + // Perform public key authorization, expand leading tilde key file paths if needed + String *const privKeyPath = + regExpMatchOne(STRDEF("^ *~"), keyPriv) ? storageSftpExpandTildePath(keyPriv) : strDup(keyPriv); + String *const pubKeyPath = + param.keyPub != NULL && + regExpMatchOne(STRDEF("^ *~"), param.keyPub) ? storageSftpExpandTildePath(param.keyPub) : strDup(param.keyPub); - do - { - rc = libssh2_userauth_publickey_fromfile( - this->session, strZ(user), strZNull(pubKeyPath), strZ(privKeyPath), strZNull(param.keyPassphrase)); - } - while (storageSftpWaitFd(this, rc)); + do + { + rc = libssh2_userauth_publickey_fromfile( + this->session, strZ(user), strZNull(pubKeyPath), strZ(privKeyPath), strZNull(param.keyPassphrase)); + } + while (storageSftpWaitFd(this, rc)); - strFree(privKeyPath); - strFree(pubKeyPath); + strFree(privKeyPath); + strFree(pubKeyPath); - if (rc != 0) - { - if (rc == LIBSSH2_ERROR_EAGAIN) - THROW_FMT(ServiceError, "timeout during public key authentication"); + if (rc != 0) + { + if (rc == LIBSSH2_ERROR_EAGAIN) + THROW_FMT(ServiceError, "timeout during public key authentication"); - storageSftpEvalLibSsh2Error( - rc, libssh2_sftp_last_error(this->sftpSession), &ServiceError, - STRDEF("public key authentication failed"), - STRDEF( - "HINT: libssh2 compiled against non-openssl libraries requires --repo-sftp-private-key-file and" - " --repo-sftp-public-key-file to be provided\n" - "HINT: libssh2 versions before 1.9.0 expect a PEM format keypair, try ssh-keygen -m PEM -t rsa -P \"\" to" - " generate the keypair")); + storageSftpEvalLibSsh2Error( + rc, libssh2_sftp_last_error(this->sftpSession), &ServiceError, + STRDEF("public key authentication failed"), + STRDEF( + "HINT: libssh2 compiled against non-openssl libraries requires --repo-sftp-private-key-file and" + " --repo-sftp-public-key-file to be provided\n" + "HINT: libssh2 versions before 1.9.0 expect a PEM format keypair, try ssh-keygen -m PEM -t rsa -P \"\"" + " to generate the keypair")); + } } // Init the sftp session diff --git a/src/storage/sftp/storage.h b/src/storage/sftp/storage.h index 5d9a558824..2cda843e5e 100644 --- a/src/storage/sftp/storage.h +++ b/src/storage/sftp/storage.h @@ -36,6 +36,7 @@ typedef struct StorageSftpNewParam StringId hostKeyCheckType; const String *hostFingerprint; const StringList *knownHosts; + const bool sshfp; } StorageSftpNewParam; #define storageSftpNewP(path, host, port, user, timeout, keyPriv, hostKeyHashType, ...) \ diff --git a/test/code-count/file-type.yaml b/test/code-count/file-type.yaml index 6f4a376c5e..5ce828b694 100644 --- a/test/code-count/file-type.yaml +++ b/test/code-count/file-type.yaml @@ -2863,6 +2863,14 @@ test/src/common/harnessServer.h: class: test/harness type: c/h +test/src/common/harnessSftpResolv.c: + class: test/harness + type: c + +test/src/common/harnessSftpResolv.h: + class: test/harness + type: c/h + test/src/common/harnessSocket.c: class: test/harness type: c diff --git a/test/data/muffat.debian.org.sshfp b/test/data/muffat.debian.org.sshfp new file mode 100644 index 0000000000..789145418f Binary files /dev/null and b/test/data/muffat.debian.org.sshfp differ diff --git a/test/data/www.postgresql.org.sshfp b/test/data/www.postgresql.org.sshfp new file mode 100644 index 0000000000..65802d74d9 Binary files /dev/null and b/test/data/www.postgresql.org.sshfp differ diff --git a/test/define.yaml b/test/define.yaml index 81acfc8f6b..a09b3ae9df 100644 --- a/test/define.yaml +++ b/test/define.yaml @@ -607,8 +607,17 @@ unit: # ---------------------------------------------------------------------------------------------------------------------------- - name: sftp - total: 19 + total: 23 harness: libSsh2 + harness: + name: sftpResolv + shim: + storage/sftp/storage: + function: + - storageSftpResNinit + - storageSftpResNquery + - storageSftpNsInitparse + - storageSftpVerifyFingerprint harness: name: fd shim: diff --git a/test/src/command/test/build.c b/test/src/command/test/build.c index 3e23524e7f..b090e885e6 100644 --- a/test/src/command/test/build.c +++ b/test/src/command/test/build.c @@ -556,6 +556,7 @@ testBldUnit(TestBuild *const this) " lib_lz4,\n" " lib_pq,\n" " lib_ssh2,\n" + " lib_resolv,\n" " lib_xml,\n" " lib_yaml,\n" " lib_z,\n" diff --git a/test/src/common/harnessSftpResolv.c b/test/src/common/harnessSftpResolv.c new file mode 100644 index 0000000000..29b65d82d7 --- /dev/null +++ b/test/src/common/harnessSftpResolv.c @@ -0,0 +1,201 @@ +/*********************************************************************************************************************************** +Harness for SFTP libresolv Testing +***********************************************************************************************************************************/ +#include "build.auto.h" + +#ifdef HAVE_LIBSSH2 + +#include +#include + +#include "common/harnessConfig.h" +#include "common/harnessDebug.h" +#include "common/harnessSftpResolv.h" + +/*********************************************************************************************************************************** +Include shimmed C modules +***********************************************************************************************************************************/ +{[SHIM_MODULE]} + +/*********************************************************************************************************************************** +Shim install state +***********************************************************************************************************************************/ +static struct +{ + bool localShimSftpResolv; // Is the shim installed? +} hrnSftpResolvStatic; + +/*********************************************************************************************************************************** +Shim storageSftpResNinit() +***********************************************************************************************************************************/ +static int +storageSftpResNinit(res_state statep) +{ + FUNCTION_HARNESS_BEGIN(); + FUNCTION_HARNESS_PARAM(VOID, statep); + FUNCTION_HARNESS_END(); + + int result; + + if (hrnSftpResolvStatic.localShimSftpResolv) + { + // Use the RES_IGNTC option to indicate when to return a failure + if ((statep->options & RES_IGNTC) == RES_IGNTC) + result = -1; + else + result = 0; + } + // Else call the normal function + else + result = storageSftpResNinit_SHIMMED(statep); + + FUNCTION_HARNESS_RETURN(INT, result); +} + +/*********************************************************************************************************************************** +Shim storageSftpResNquery() +***********************************************************************************************************************************/ +static int +storageSftpResNquery( + res_state statep, const char *const dname, const int class, const int type, unsigned char *answer, + const int anslen) +{ + FUNCTION_HARNESS_BEGIN(); + FUNCTION_HARNESS_PARAM_P(VOID, statep); + FUNCTION_HARNESS_PARAM(STRINGZ, dname); + FUNCTION_HARNESS_PARAM(INT, class); + FUNCTION_HARNESS_PARAM(INT, type); + FUNCTION_HARNESS_PARAM_P(UCHARDATA, answer); + FUNCTION_HARNESS_PARAM(INT, anslen); + FUNCTION_HARNESS_END(); + + int result; + + // Avoid compiler complaining of unused params + (void)statep; + (void)dname; + (void)class; + (void)type; + (void)answer; + (void)anslen; + + if (hrnSftpResolvStatic.localShimSftpResolv) + { + // Overlay HEADER onto the answer buffer + HEADER *header = (HEADER *)answer; + + if (strcmp(dname, "trustad-fail") == 0) + { + // Unset the ad flag to indicate that the answer is not authenticated + result = 0; + header->ad = 0; + } + else if (strcmp(dname, "trustad-pass") == 0 || strcmp(dname, "localhost") == 0) + { + // Set the ad flag to indicate that the answer is authenticated + result = 1; + header->ad = 1; + } + else + { + // Return an error + result = -1; + statep->res_h_errno = NO_DATA; + header->ad = 0; + } + } + // Else call the normal function + else + result = storageSftpResNquery_SHIMMED(statep, dname, class, type, answer, anslen); + + FUNCTION_HARNESS_RETURN(INT, result); +} + +/*********************************************************************************************************************************** +Shim storageSftpNsInitparse() +***********************************************************************************************************************************/ +static int +storageSftpNsInitparse(const unsigned char *answer, int len, ns_msg *handle) +{ + FUNCTION_HARNESS_BEGIN(); + FUNCTION_HARNESS_PARAM_P(UCHARDATA, answer); + FUNCTION_HARNESS_PARAM(INT, len); + FUNCTION_HARNESS_PARAM_P(VOID, handle); + FUNCTION_HARNESS_END(); + + int result; + + if (hrnSftpResolvStatic.localShimSftpResolv) + { + // Use the incoming len from the test call to determine the result + switch (len) + { + case 0: + result = 0; + break; + + case 1: + result = 0; + break; + + default: + result = -1; + break; + } + } + else + result = storageSftpNsInitparse_SHIMMED(answer, len, handle); + + FUNCTION_HARNESS_RETURN(INT, result); +} + +/*********************************************************************************************************************************** +Shim storageSftpVerifyFingerprint() +***********************************************************************************************************************************/ +static bool +storageSftpVerifyFingerprint(LIBSSH2_SESSION *const session, ns_msg handle) +{ + FUNCTION_HARNESS_BEGIN(); + FUNCTION_HARNESS_PARAM_P(VOID, session); + FUNCTION_HARNESS_PARAM(VOID, handle); + FUNCTION_HARNESS_END(); + + bool result; + + if (session == NULL) + THROW(AssertError, "storageSftpVerifyFingerprint expects 'session' to be not null"); + + if (hrnSftpResolvStatic.localShimSftpResolv) + { + // Return true. storageSftpVerifyFingerprint has its own tests in sftpTest.c. + result = true; + } + else + result = storageSftpVerifyFingerprint_SHIMMED(session, handle); + + FUNCTION_HARNESS_RETURN(BOOL, result); +} + +/**********************************************************************************************************************************/ +void +hrnSftpResolvShimInstall(void) +{ + FUNCTION_HARNESS_VOID(); + + hrnSftpResolvStatic.localShimSftpResolv = true; + + FUNCTION_HARNESS_RETURN_VOID(); +} + +/**********************************************************************************************************************************/ +void +hrnSftpResolvShimUninstall(void) +{ + FUNCTION_HARNESS_VOID(); + + hrnSftpResolvStatic.localShimSftpResolv = false; + + FUNCTION_HARNESS_RETURN_VOID(); +} + +#endif // HAVE_LIBSSH2 diff --git a/test/src/common/harnessSftpResolv.h b/test/src/common/harnessSftpResolv.h new file mode 100644 index 0000000000..01b3a21674 --- /dev/null +++ b/test/src/common/harnessSftpResolv.h @@ -0,0 +1,12 @@ +/*********************************************************************************************************************************** +Harness for SFTP libresolv Testing +***********************************************************************************************************************************/ + +/*********************************************************************************************************************************** +Functions +***********************************************************************************************************************************/ +#ifdef HAVE_LIBSSH2 +// Install/uninstall shim +void hrnSftpResolvShimInstall(void); +void hrnSftpResolvShimUninstall(void); +#endif // HAVE_LIBSSH2 diff --git a/test/src/module/command/helpTest.c b/test/src/module/command/helpTest.c index b37879752b..ae34355476 100644 --- a/test/src/module/command/helpTest.c +++ b/test/src/module/command/helpTest.c @@ -332,6 +332,7 @@ testRun(void) " --repo-sftp-private-key-file SFTP private key file\n" " --repo-sftp-private-key-passphrase SFTP private key passphrase\n" " --repo-sftp-public-key-file SFTP public key file\n" + " --repo-sftp-verify-via-sshfp SFTP verify via SSHFP [default=n]\n" " --repo-storage-ca-file repository storage CA file\n" " --repo-storage-ca-path repository storage CA path\n" " --repo-storage-host repository storage host\n" diff --git a/test/src/module/storage/sftpTest.c b/test/src/module/storage/sftpTest.c index 4846e417b4..92a3bf69a4 100644 --- a/test/src/module/storage/sftpTest.c +++ b/test/src/module/storage/sftpTest.c @@ -14,6 +14,7 @@ Test SFTP Storage #include "common/harnessFd.h" #include "common/harnessFork.h" #include "common/harnessLibSsh2.h" +#include "common/harnessSftpResolv.h" #include "common/harnessSocket.h" #include "common/harnessStorage.h" @@ -1134,6 +1135,7 @@ testRun(void) .keyPassphrase = cfgOptionIdxStrNull(cfgOptRepoSftpPrivateKeyPassphrase, repoIdx), .hostFingerprint = cfgOptionIdxStrNull(cfgOptRepoSftpHostFingerprint, repoIdx), .hostKeyCheckType = cfgOptionIdxStrId(cfgOptRepoSftpHostKeyCheckType, repoIdx), + .sshfp = cfgOptionIdxBool(cfgOptRepoSftpVerifyViaSshfp, repoIdx), .knownHosts = strLstNewVarLst(cfgOptionIdxLst(cfgOptRepoSftpKnownHost, repoIdx))), "new storage (defaults)"); TEST_RESULT_LOG( @@ -1150,6 +1152,301 @@ testRun(void) memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("sftp session init success - add host to known_hosts file RSA, sshfp, res_nquery fail, shimmed"); + + // Install shim for SFTP libresolv functions + hrnSftpResolvShimInstall(); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_KNOWNHOST_INIT}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" KNOWNHOSTS2_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERROR, .errMsg = (char *)"Failed to open file", .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" ETC_KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERROR, .errMsg = (char *)"Failed to open file", .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" ETC_KNOWNHOSTS2_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERROR, .errMsg = (char *)"Failed to open file", .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_HOSTKEY, .len = 20, .type = LIBSSH2_HOSTKEY_TYPE_RSA, .resultZ = HOSTKEY}, + {.function = HRNLIBSSH2_KNOWNHOST_CHECKP, .param = "[\"no-data\",22,\"" HOSTKEY "\",20,65537]", + .resultInt = LIBSSH2_KNOWNHOST_CHECK_NOTFOUND}, + {.function = HRNLIBSSH2_KNOWNHOST_INIT}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_KNOWNHOST_ADDC, + .param = "[\"no-data\",null,\"12345678901234567890\",20,\"Generated from pgBackRest\",25,589825]"}, + {.function = HRNLIBSSH2_KNOWNHOST_WRITEFILE, .param = "[\"" KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", + .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_INIT}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + argList = strLstNew(); + hrnCfgArgRawZ(argList, cfgOptStanza, "test"); + hrnCfgArgRawZ(argList, cfgOptPgPath, "/path/to/pg"); + hrnCfgArgRawZ(argList, cfgOptRepo, "1"); + hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostUser, TEST_USER); + hrnCfgArgRawZ(argList, cfgOptRepoType, "sftp"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHost, "no-data"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyHashType, "sha1"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPrivateKeyFile, KEYPRIV_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPublicKeyFile, KEYPUB_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyCheckType, "accept-new"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpVerifyViaSshfp, "y"); + HRN_CFG_LOAD(cfgCmdArchiveGet, argList); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + cfgOptionIdxStr(cfgOptRepoPath, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHost, repoIdx), + cfgOptionIdxUInt(cfgOptRepoSftpHostPort, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHostUser, repoIdx), + cfgOptionUInt64(cfgOptIoTimeout), cfgOptionIdxStr(cfgOptRepoSftpPrivateKeyFile, repoIdx), + cfgOptionIdxStrId(cfgOptRepoSftpHostKeyHashType, repoIdx), .modeFile = STORAGE_MODE_FILE_DEFAULT, + .modePath = STORAGE_MODE_PATH_DEFAULT, .keyPub = cfgOptionIdxStrNull(cfgOptRepoSftpPublicKeyFile, repoIdx), + .keyPassphrase = cfgOptionIdxStrNull(cfgOptRepoSftpPrivateKeyPassphrase, repoIdx), + .hostFingerprint = cfgOptionIdxStrNull(cfgOptRepoSftpHostFingerprint, repoIdx), + .hostKeyCheckType = cfgOptionIdxStrId(cfgOptRepoSftpHostKeyCheckType, repoIdx), + .sshfp = cfgOptionIdxBool(cfgOptRepoSftpVerifyViaSshfp, repoIdx), + .knownHosts = strLstNewVarLst(cfgOptionIdxLst(cfgOptRepoSftpKnownHost, repoIdx))), + "new storage (defaults)"); + TEST_RESULT_LOG( + "P00 WARN: res_nquery error [4] No address associated with name 'no-data'\n" + "P00 WARN: Host cannot be verified via SSHFP, RES_TRUSTAD not set in response\n" +#ifndef RES_TRUSTAD + "P00 WARN: RES_TRUSTAD not supported on this OS, host 'no-data' cannot be verified via SSHFP\n" +#endif // RES_TRUSTAD + "P00 WARN: ns_initparse error [-1] Resolver internal error for host 'no-data'\n" + "P00 WARN: host 'no-data' not found in known hosts files, attempting to add host to " + "'/home/" TEST_USER "/.ssh/known_hosts'\n" + "P00 WARN: pgBackRest added new host 'no-data' to '/home/" TEST_USER "/.ssh/known_hosts'"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("sftp session init success - add host to known_hosts file RSA, sshfp fail"); + +#ifdef RES_TRUSTAD + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_KNOWNHOST_INIT}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" KNOWNHOSTS2_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERROR, .errMsg = (char *)"Failed to open file", .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" ETC_KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERROR, .errMsg = (char *)"Failed to open file", .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" ETC_KNOWNHOSTS2_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERROR, .errMsg = (char *)"Failed to open file", .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_HOSTKEY, .len = 20, .type = LIBSSH2_HOSTKEY_TYPE_RSA, .resultZ = HOSTKEY}, + {.function = HRNLIBSSH2_KNOWNHOST_CHECKP, .param = "[\"trustad-fail\",22,\"" HOSTKEY "\",20,65537]", + .resultInt = LIBSSH2_KNOWNHOST_CHECK_NOTFOUND}, + {.function = HRNLIBSSH2_KNOWNHOST_INIT}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_KNOWNHOST_ADDC, + .param = "[\"trustad-fail\",null,\"12345678901234567890\",20,\"Generated from pgBackRest\",25,589825]"}, + {.function = HRNLIBSSH2_KNOWNHOST_WRITEFILE, .param = "[\"" KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", + .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_INIT}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + argList = strLstNew(); + hrnCfgArgRawZ(argList, cfgOptStanza, "test"); + hrnCfgArgRawZ(argList, cfgOptPgPath, "/path/to/pg"); + hrnCfgArgRawZ(argList, cfgOptRepo, "1"); + hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostUser, TEST_USER); + hrnCfgArgRawZ(argList, cfgOptRepoType, "sftp"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHost, "trustad-fail"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyHashType, "sha1"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPrivateKeyFile, KEYPRIV_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPublicKeyFile, KEYPUB_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyCheckType, "accept-new"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpVerifyViaSshfp, "y"); + HRN_CFG_LOAD(cfgCmdArchiveGet, argList); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + cfgOptionIdxStr(cfgOptRepoPath, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHost, repoIdx), + cfgOptionIdxUInt(cfgOptRepoSftpHostPort, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHostUser, repoIdx), + cfgOptionUInt64(cfgOptIoTimeout), cfgOptionIdxStr(cfgOptRepoSftpPrivateKeyFile, repoIdx), + cfgOptionIdxStrId(cfgOptRepoSftpHostKeyHashType, repoIdx), .modeFile = STORAGE_MODE_FILE_DEFAULT, + .modePath = STORAGE_MODE_PATH_DEFAULT, .keyPub = cfgOptionIdxStrNull(cfgOptRepoSftpPublicKeyFile, repoIdx), + .keyPassphrase = cfgOptionIdxStrNull(cfgOptRepoSftpPrivateKeyPassphrase, repoIdx), + .hostFingerprint = cfgOptionIdxStrNull(cfgOptRepoSftpHostFingerprint, repoIdx), + .hostKeyCheckType = cfgOptionIdxStrId(cfgOptRepoSftpHostKeyCheckType, repoIdx), + .sshfp = cfgOptionIdxBool(cfgOptRepoSftpVerifyViaSshfp, repoIdx), + .knownHosts = strLstNewVarLst(cfgOptionIdxLst(cfgOptRepoSftpKnownHost, repoIdx))), + "new storage (defaults)"); + TEST_RESULT_LOG( + "P00 WARN: Host cannot be verified via SSHFP, RES_TRUSTAD not set in response\n" + "P00 WARN: host 'trustad-fail' not found in known hosts files, attempting to add host to " + "'/home/" TEST_USER "/.ssh/known_hosts'\n" + "P00 WARN: pgBackRest added new host 'trustad-fail' to '/home/" TEST_USER "/.ssh/known_hosts'"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); +#else + TEST_LOG("RES_TRUSTAD not supported by OS"); +#endif // RES_TRUSTAD + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("sftp session init success - add host to known_hosts file RSA, sshfp pass"); + +#ifdef RES_TRUSTAD + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_INIT}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + argList = strLstNew(); + hrnCfgArgRawZ(argList, cfgOptStanza, "test"); + hrnCfgArgRawZ(argList, cfgOptPgPath, "/path/to/pg"); + hrnCfgArgRawZ(argList, cfgOptRepo, "1"); + hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostUser, TEST_USER); + hrnCfgArgRawZ(argList, cfgOptRepoType, "sftp"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHost, "trustad-pass"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyHashType, "sha1"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPrivateKeyFile, KEYPRIV_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPublicKeyFile, KEYPUB_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyCheckType, "accept-new"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpVerifyViaSshfp, "y"); + HRN_CFG_LOAD(cfgCmdArchiveGet, argList); + + storageTest = NULL; + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + cfgOptionIdxStr(cfgOptRepoPath, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHost, repoIdx), + cfgOptionIdxUInt(cfgOptRepoSftpHostPort, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHostUser, repoIdx), + cfgOptionUInt64(cfgOptIoTimeout), cfgOptionIdxStr(cfgOptRepoSftpPrivateKeyFile, repoIdx), + cfgOptionIdxStrId(cfgOptRepoSftpHostKeyHashType, repoIdx), .modeFile = STORAGE_MODE_FILE_DEFAULT, + .modePath = STORAGE_MODE_PATH_DEFAULT, .keyPub = cfgOptionIdxStrNull(cfgOptRepoSftpPublicKeyFile, repoIdx), + .keyPassphrase = cfgOptionIdxStrNull(cfgOptRepoSftpPrivateKeyPassphrase, repoIdx), + .hostFingerprint = cfgOptionIdxStrNull(cfgOptRepoSftpHostFingerprint, repoIdx), + .hostKeyCheckType = cfgOptionIdxStrId(cfgOptRepoSftpHostKeyCheckType, repoIdx), + .sshfp = cfgOptionIdxBool(cfgOptRepoSftpVerifyViaSshfp, repoIdx), + .knownHosts = strLstNewVarLst(cfgOptionIdxLst(cfgOptRepoSftpKnownHost, repoIdx))), + "new storage (defaults)"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); +#else + TEST_LOG("RES_TRUSTAD not supported by OS"); +#endif // RES_TRUSTAD + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("sftp session init success - add host to known_hosts file RSA, storageSftpResInit fail"); + +#ifdef RES_TRUSTAD + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_KNOWNHOST_INIT}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" KNOWNHOSTS2_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERROR, .errMsg = (char *)"Failed to open file", .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" ETC_KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERROR, .errMsg = (char *)"Failed to open file", .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" ETC_KNOWNHOSTS2_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERROR, .errMsg = (char *)"Failed to open file", .resultInt = LIBSSH2_ERROR_FILE}, + {.function = HRNLIBSSH2_SESSION_HOSTKEY, .len = 20, .type = LIBSSH2_HOSTKEY_TYPE_RSA, .resultZ = HOSTKEY}, + {.function = HRNLIBSSH2_KNOWNHOST_CHECKP, .param = "[\"trustad-fail\",22,\"" HOSTKEY "\",20,65537]", + .resultInt = LIBSSH2_KNOWNHOST_CHECK_NOTFOUND}, + {.function = HRNLIBSSH2_KNOWNHOST_INIT}, + {.function = HRNLIBSSH2_KNOWNHOST_READFILE, .param = "[\"" KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_KNOWNHOST_ADDC, + .param = "[\"trustad-fail\",null,\"12345678901234567890\",20,\"Generated from pgBackRest\",25,589825]"}, + {.function = HRNLIBSSH2_KNOWNHOST_WRITEFILE, .param = "[\"" KNOWNHOSTS_FILE_CSTR "\",1]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", + .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_INIT}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Use the RES_IGNTC option indicate when to return a failure from res_ninit when shimmed + my_res_state.options |= RES_IGNTC; + + argList = strLstNew(); + hrnCfgArgRawZ(argList, cfgOptStanza, "test"); + hrnCfgArgRawZ(argList, cfgOptPgPath, "/path/to/pg"); + hrnCfgArgRawZ(argList, cfgOptRepo, "1"); + hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostUser, TEST_USER); + hrnCfgArgRawZ(argList, cfgOptRepoType, "sftp"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHost, "trustad-fail"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyHashType, "sha1"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPrivateKeyFile, KEYPRIV_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPublicKeyFile, KEYPUB_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyCheckType, "accept-new"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpVerifyViaSshfp, "y"); + HRN_CFG_LOAD(cfgCmdArchiveGet, argList); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + cfgOptionIdxStr(cfgOptRepoPath, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHost, repoIdx), + cfgOptionIdxUInt(cfgOptRepoSftpHostPort, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHostUser, repoIdx), + cfgOptionUInt64(cfgOptIoTimeout), cfgOptionIdxStr(cfgOptRepoSftpPrivateKeyFile, repoIdx), + cfgOptionIdxStrId(cfgOptRepoSftpHostKeyHashType, repoIdx), .modeFile = STORAGE_MODE_FILE_DEFAULT, + .modePath = STORAGE_MODE_PATH_DEFAULT, .keyPub = cfgOptionIdxStrNull(cfgOptRepoSftpPublicKeyFile, repoIdx), + .keyPassphrase = cfgOptionIdxStrNull(cfgOptRepoSftpPrivateKeyPassphrase, repoIdx), + .hostFingerprint = cfgOptionIdxStrNull(cfgOptRepoSftpHostFingerprint, repoIdx), + .hostKeyCheckType = cfgOptionIdxStrId(cfgOptRepoSftpHostKeyCheckType, repoIdx), + .sshfp = cfgOptionIdxBool(cfgOptRepoSftpVerifyViaSshfp, repoIdx), + .knownHosts = strLstNewVarLst(cfgOptionIdxLst(cfgOptRepoSftpKnownHost, repoIdx))), + "new storage (defaults)"); + TEST_RESULT_LOG( + "P00 WARN: unable to initialize resolver\n" + "P00 WARN: Host cannot be verified via SSHFP, RES_TRUSTAD not set in response\n" + "P00 WARN: host 'trustad-fail' not found in known hosts files, attempting to add host to " + "'/home/" TEST_USER "/.ssh/known_hosts'\n" + "P00 WARN: pgBackRest added new host 'trustad-fail' to '/home/" TEST_USER "/.ssh/known_hosts'"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); +#else + TEST_LOG("RES_TRUSTAD not supported by OS"); +#endif // RES_TRUSTAD + + // Uninstall shim for SFTP libresolv functions + hrnSftpResolvShimUninstall(); + // ------------------------------------------------------------------------------------------------------------------------- TEST_TITLE("sftp session init success - hostKeyCheckType = accept-new - add host to user's known_hosts file DSS"); @@ -1184,6 +1481,21 @@ testRun(void) HRNLIBSSH2_MACRO_SHUTDOWN() }); + argList = strLstNew(); + hrnCfgArgRawZ(argList, cfgOptStanza, "test"); + hrnCfgArgRawZ(argList, cfgOptPgPath, "/path/to/pg"); + hrnCfgArgRawZ(argList, cfgOptRepo, "1"); + hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostUser, TEST_USER); + hrnCfgArgRawZ(argList, cfgOptRepoType, "sftp"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHost, "localhost"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyHashType, "sha1"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPrivateKeyFile, KEYPRIV_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPublicKeyFile, KEYPUB_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyCheckType, "accept-new"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpVerifyViaSshfp, "y"); + HRN_CFG_LOAD(cfgCmdArchiveGet, argList); + storageTest = NULL; TEST_ASSIGN( @@ -7842,6 +8154,675 @@ testRun(void) #endif // HAVE_LIBSSH2 } + // ***************************************************************************************************************************** + if (testBegin("storageSftpVerifyFingerprint()")) + { +#ifdef HAVE_LIBSSH2 + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpVerifyFingerprint() no sshfp fingerprint records"); + + harnessLogLevelSet(logLevelDetail); + + TimeMSec timeout = 500; + const StorageSftpNewParam param = {.sshfp = true}; + + // Configure a valid host so that we can successfully initialize the resolver + const String *host = STRDEF("www.postgresql.org"); + unsigned int port = 22; + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = 0}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = 0}, + {.function = NULL}, + }); + + // Step through the sftp storage startup process + OBJ_NEW_BEGIN(StorageSftp, .childQty = MEM_CONTEXT_QTY_MAX, .callbackQty = 1) + { + *this = (StorageSftp) + { + .interface = storageInterfaceSftp, + .timeout = timeout, + }; + + // Init SFTP session + if (libssh2_init(0) != 0) + THROW_FMT(ServiceError, "unable to init libssh2"); + + this->ioSession = ioClientOpen(sckClientNew(host, port, timeout, timeout)); + this->session = libssh2_session_init(); + + if (this->session == NULL) + THROW_FMT(ServiceError, "unable to init libssh2 session"); + + // Set session to non-blocking + libssh2_session_set_blocking(this->session, 0); + + // Perform handshake + int rc; + + do + { + rc = libssh2_session_handshake(this->session, ioSessionFd(this->ioSession)); + } + while (storageSftpWaitFd(this, rc)); + + if (rc == LIBSSH2_ERROR_EAGAIN) + THROW_FMT(ServiceError, "timeout during libssh2 handshake [%d]", rc); + + if (rc != 0) + THROW_FMT(ServiceError, "libssh2 handshake failed [%d]", rc); + + if (param.sshfp) + { + if (storageSftpResNinit(&my_res_state) != 0) + LOG_WARN("unable to initialize resolver"); + +#ifdef RES_TRUSTAD + // Set the resolver to use TRUSTAD + storageSftpSetOption(&my_res_state, RES_TRUSTAD); +#endif // RES_TRUSTAD + + // Query the server for SSHFP records + unsigned char answer[PACKET_SZ]; + + int len = storageSftpResNquery(&my_res_state, strZ(host), C_IN, T_SSHFP, answer, sizeof(answer)); + + // Check for errors. + // This is dependent on keeping the _DEFAULT_SOURCE for netdb.h. We can drop it and rewrite to a generic error if we + // think that's better. + if (len < 0) + { + LOG_WARN_FMT( + "res_nquery error [%d] %s '%s'", my_res_state.res_h_errno, hstrerror(my_res_state.res_h_errno), strZ(host)); + } + + // Overwrite the sshfp response with a known defined response for testing - this response has ad set, but no sshfp + // records + Buffer *sshfp = + storageGetP(storageNewReadP(storagePosixNewP(HRN_PATH_REPO_STR), STRDEF("test/data/www.postgresql.org.sshfp"))); + + // Verify we got the expected size + TEST_RESULT_INT((int)bufUsed(sshfp), 112, "expected size 112"); + + memset(answer, 0, sizeof(answer)); + memmove(answer, bufPtr(sshfp), (size_t)bufUsed(sshfp)); + len = (int)bufUsed(sshfp); + + // Default res_trustad to unset + unsigned char res_trustad = 0; +#ifdef RES_TRUSTAD + // Check the RES_TRUSTAD flag + res_trustad = ((HEADER *)answer)->ad; +#endif // RES_TRUSTAD + + if (res_trustad != 1) + LOG_WARN("Host cannot be verified via SSHFP, RES_TRUSTAD not set in response"); + +#ifndef RES_TRUSTAD + LOG_WARN_FMT("RES_TRUSTAD not supported on this OS, host '%s' cannot be verified via SSHFP", strZ(host)); +#endif // RES_TRUSTAD + + // Initialize parsing the response + int rc; + ns_msg handle; + if ((rc = storageSftpNsInitparse(answer, len, &handle)) != 0) + LOG_WARN_FMT("ns_initparse error [%d] %s for host '%s'", rc, hstrerror(rc), strZ(host)); + + // Attempt to verify the host via DNS provided fingerprint - host key not found in SSHFP record + TEST_RESULT_BOOL( + storageSftpVerifyFingerprint(this->session, handle), false, "return false, host key not found in SSHFP record"); + + // Close the resolver + res_nclose(&my_res_state); + } + } + OBJ_NEW_END(); + + objFree(this); + + TEST_RESULT_LOG( +#ifndef RES_TRUSTAD + "P00 WARN: Host cannot be verified via SSHFP, RES_TRUSTAD not set in response\n" + "P00 WARN: RES_TRUSTAD not supported on this OS, host 'www.postgresql.org' cannot be verified via SSHFP\n" +#endif // RES_TRUSTAD + "P00 WARN: no SSHFP records for host found in DNS"); + + harnessLogLevelReset(); +#else + TEST_LOG(PROJECT_NAME " not built with sftp support"); +#endif // HAVE_LIBSSH2 + } + + // ***************************************************************************************************************************** + if (testBegin("storageSftpVerifyFingerprint()")) + { +#ifdef HAVE_LIBSSH2 + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpVerifyFingerprint() fail"); + + harnessLogLevelSet(logLevelDetail); + + TimeMSec timeout = 500; + const StorageSftpNewParam param = {.sshfp = true}; + + // Configure a valid host so that we can successfully initialize the resolver + const String *host = STRDEF("www.postgresql.org"); + unsigned int port = 22; + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = 0}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = 0}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultNull = true}, +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[3]", .resultZ = HOSTKEY}, +#else + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = HOSTKEY}, +#endif // LIBSSH2_HOSTKEY_HASH_SHA256 +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[3]", .resultNull = true}, +#else + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultNull = true}, +#endif // LIBSSH2_HOSTKEY_HASH_SHA256 + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultNull = true}, + {.function = NULL}, + }); + + // Step through the sftp storage startup process + OBJ_NEW_BEGIN(StorageSftp, .childQty = MEM_CONTEXT_QTY_MAX, .callbackQty = 1) + { + *this = (StorageSftp) + { + .interface = storageInterfaceSftp, + .timeout = timeout, + }; + + // Init SFTP session + if (libssh2_init(0) != 0) + THROW_FMT(ServiceError, "unable to init libssh2"); + + this->ioSession = ioClientOpen(sckClientNew(host, port, timeout, timeout)); + this->session = libssh2_session_init(); + + if (this->session == NULL) + THROW_FMT(ServiceError, "unable to init libssh2 session"); + + // Set session to non-blocking + libssh2_session_set_blocking(this->session, 0); + + // Perform handshake + int rc; + + do + { + rc = libssh2_session_handshake(this->session, ioSessionFd(this->ioSession)); + } + while (storageSftpWaitFd(this, rc)); + + if (rc == LIBSSH2_ERROR_EAGAIN) + THROW_FMT(ServiceError, "timeout during libssh2 handshake [%d]", rc); + + if (rc != 0) + THROW_FMT(ServiceError, "libssh2 handshake failed [%d]", rc); + + if (param.sshfp) + { + if (storageSftpResNinit(&my_res_state) != 0) + LOG_WARN("unable to initialize resolver"); + +#ifdef RES_TRUSTAD + // Set the resolver to use TRUSTAD + storageSftpSetOption(&my_res_state, RES_TRUSTAD); +#endif // RES_TRUSTAD + + // Query the server for SSHFP records + unsigned char answer[PACKET_SZ]; + + int len = storageSftpResNquery(&my_res_state, strZ(host), C_IN, T_SSHFP, answer, sizeof(answer)); + + // Check for errors. + // This is dependent on keeping the _DEFAULT_SOURCE for netdb.h. We can drop it and rewrite to a generic error if we + // think that's better. + if (len < 0) + { + LOG_WARN_FMT( + "res_nquery error [%d] %s '%s'", my_res_state.res_h_errno, hstrerror(my_res_state.res_h_errno), strZ(host)); + } + + // Overwrite the sshfp response with a known defined response for testing - this response has ad set and 4 sshfp + // records + Buffer *sshfp = + storageGetP(storageNewReadP(storagePosixNewP(HRN_PATH_REPO_STR), STRDEF("test/data/muffat.debian.org.sshfp"))); + + // Verify we got the expected size + TEST_RESULT_INT((int)bufUsed(sshfp), 195, "expected size 195"); + + memset(answer, 0, sizeof(answer)); + memmove(answer, bufPtr(sshfp), (size_t)bufUsed(sshfp)); + len = (int)bufUsed(sshfp); + + // Default res_trustad to unset + unsigned char res_trustad = 0; +#ifdef RES_TRUSTAD + // Check the RES_TRUSTAD flag + res_trustad = ((HEADER *)answer)->ad; +#endif // RES_TRUSTAD + + if (res_trustad != 1) + LOG_WARN("Host cannot be verified, RES_TRUSTAD not set in response"); + +#ifndef RES_TRUSTAD + LOG_WARN_FMT("RES_TRUSTAD not supported on this OS, host '%s' cannot be verified via SSHFP", strZ(host)); +#endif // RES_TRUSTAD + + // Initialize parsing the response + int rc; + ns_msg handle; + if ((rc = storageSftpNsInitparse(answer, len, &handle)) != 0) + LOG_WARN_FMT("ns_initparse error [%d] %s for host '%s'", rc, hstrerror(rc), strZ(host)); + + // Attempt to verify the host via DNS provided fingerprint -- no keys match + TEST_RESULT_BOOL(storageSftpVerifyFingerprint(this->session, handle), false, "no keys match"); + + // Close the resolver + res_nclose(&my_res_state); + } + } + OBJ_NEW_END(); + + objFree(this); + + TEST_RESULT_LOG( +#ifndef RES_TRUSTAD + "P00 WARN: Host cannot be verified, RES_TRUSTAD not set in response\n" + "P00 WARN: RES_TRUSTAD not supported on this OS, host 'www.postgresql.org' cannot be verified via SSHFP\n" +#endif // RES_TRUSTAD + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [1] hashType [2]" + " 'bdc1f467ab69238fc4173c20658097835379dbe5'\n" +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [2] hashType [3]" + " 'cf40a796b1e8775e60a77d410db745012e13410935489c411dbfcadf9d62de19'\n" + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [2] hashType [3]" + " 'ded38fadb5713bc6c772e788b5cc41223ca4072c061e5ef152b63ebb1b024096'\n" +#else + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [2] hashType [2]" + " 'cf40a796b1e8775e60a77d410db745012e134109'\n" + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [2] hashType [2]" + " 'ded38fadb5713bc6c772e788b5cc41223ca4072c'\n" +#endif // LIBSSH2_HOSTKEY_HASH_SHA256 + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [1] hashType [2]" + " '87ac6bede384d2dc6254f396b83ed34856512e64'"); + + harnessLogLevelReset(); +#else + TEST_LOG(PROJECT_NAME " not built with sftp support"); +#endif // HAVE_LIBSSH2 + } + + // ***************************************************************************************************************************** + if (testBegin("storageSftpVerifyFingerprint()")) + { +#ifdef HAVE_LIBSSH2 + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpVerifyFingerprint() keyMatch"); + + harnessLogLevelSet(logLevelDetail); + + TimeMSec timeout = 500; + const StorageSftpNewParam param = {.sshfp = true}; + + // Configure a valid host so that we can successfully initialize the resolver + const String *host = STRDEF("www.postgresql.org"); + unsigned int port = 22; + + // Create binary representation of the host keys that will generate a successful match + unsigned char fingerprint1[1024]; + decodeToBin(encodingHex, "BDC1F467AB69238FC4173C20658097835379DBE5", fingerprint1); + unsigned char fingerprint2[1024]; + decodeToBin(encodingHex, "CF40A796B1E8775E60A77D410DB745012E13410935489C411DBFCADF9D62DE19", fingerprint2); + unsigned char fingerprint3[1024]; + decodeToBin(encodingHex, "DED38FADB5713BC6C772E788B5CC41223CA4072C061E5EF152B63EBB1B024096", fingerprint3); + unsigned char fingerprint4[1024]; + decodeToBin(encodingHex, "87ac6bede384d2dc6254f396b83ed34856512e64", fingerprint4); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = 0}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = 0}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = (char *)fingerprint1}, +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[3]", .resultZ = (char *)fingerprint2}, +#else + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = (char *)fingerprint2}, +#endif // LIBSSH2_HOSTKEY_HASH_SHA256 +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[3]", .resultZ = (char *)fingerprint3}, +#else + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = (char *)fingerprint3}, +#endif // LIBSSH2_HOSTKEY_HASH_SHA256 + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = (char *)fingerprint4}, + {.function = NULL}, + }); + + OBJ_NEW_BEGIN(StorageSftp, .childQty = MEM_CONTEXT_QTY_MAX, .callbackQty = 1) + { + *this = (StorageSftp) + { + .interface = storageInterfaceSftp, + .timeout = timeout, + }; + + // Init SFTP session + if (libssh2_init(0) != 0) + THROW_FMT(ServiceError, "unable to init libssh2"); + + this->ioSession = ioClientOpen(sckClientNew(host, port, timeout, timeout)); + this->session = libssh2_session_init(); + + if (this->session == NULL) + THROW_FMT(ServiceError, "unable to init libssh2 session"); + + // Set session to non-blocking + libssh2_session_set_blocking(this->session, 0); + + // Perform handshake + int rc; + + do + { + rc = libssh2_session_handshake(this->session, ioSessionFd(this->ioSession)); + } + while (storageSftpWaitFd(this, rc)); + + if (rc == LIBSSH2_ERROR_EAGAIN) + THROW_FMT(ServiceError, "timeout during libssh2 handshake [%d]", rc); + + if (rc != 0) + THROW_FMT(ServiceError, "libssh2 handshake failed [%d]", rc); + + if (param.sshfp) + { + if (storageSftpResNinit(&my_res_state) != 0) + LOG_WARN("unable to initialize resolver"); + +#ifdef RES_TRUSTAD + // Set the resolver to use TRUSTAD + storageSftpSetOption(&my_res_state, RES_TRUSTAD); +#endif // RES_TRUSTAD + + // Query the server for SSHFP records + unsigned char answer[PACKET_SZ]; + + int len = storageSftpResNquery(&my_res_state, strZ(host), C_IN, T_SSHFP, answer, sizeof(answer)); + + // Check for errors. + // This is dependent on keeping the _DEFAULT_SOURCE for netdb.h. We can drop it and rewrite to a generic error if we + // think that's better. + if (len < 0) + { + LOG_WARN_FMT( + "res_nquery error [%d] %s '%s'", my_res_state.res_h_errno, hstrerror(my_res_state.res_h_errno), strZ(host)); + } + + // Overwrite the sshfp response with a known defined response for testing + Buffer *sshfp = + storageGetP(storageNewReadP(storagePosixNewP(HRN_PATH_REPO_STR), STRDEF("test/data/muffat.debian.org.sshfp"))); + + // Verify we got the expected size + TEST_RESULT_INT((int)bufUsed(sshfp), 195, "expected size"); + + memset(answer, 0, sizeof(answer)); + memmove(answer, bufPtr(sshfp), (size_t)bufUsed(sshfp)); + len = (int)bufUsed(sshfp); + + // Default res_trustad to unset + unsigned char res_trustad = 0; +#ifdef RES_TRUSTAD + // Check the RES_TRUSTAD flag + res_trustad = ((HEADER *)answer)->ad; +#endif // RES_TRUSTAD + + if (res_trustad != 1) + LOG_WARN("Host is untrusted, RES_TRUSTAD not set in response"); + +#ifndef RES_TRUSTAD + LOG_WARN_FMT("RES_TRUSTAD not supported on this OS, host '%s' cannot be verified via SSHFP", strZ(host)); +#endif // RES_TRUSTAD + + // Initialize parsing the response + int rc; + ns_msg handle; + if ((rc = storageSftpNsInitparse(answer, len, &handle)) != 0) + LOG_WARN_FMT("ns_initparse error [%d] %s for host '%s'", rc, hstrerror(rc), strZ(host)); + + // Attempt to verify the host via DNS provided fingerprint -- all keys match + TEST_RESULT_BOOL(storageSftpVerifyFingerprint(this->session, handle), true, "all keys match in SSHFP record"); + + // Close the resolver + res_nclose(&my_res_state); + } + } + OBJ_NEW_END(); + + objFree(this); + + TEST_RESULT_LOG( +#ifndef RES_TRUSTAD + "P00 WARN: Host is untrusted, RES_TRUSTAD not set in response\n" + "P00 WARN: RES_TRUSTAD not supported on this OS, host 'www.postgresql.org' cannot be verified via SSHFP\n" +#endif // RES_TRUSTAD + "P00 DETAIL: sshfp fingerprint match found for sshfp digest_type [1] hashType [2]" + " 'bdc1f467ab69238fc4173c20658097835379dbe5'\n" +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + "P00 DETAIL: sshfp fingerprint match found for sshfp digest_type [2] hashType [3]" + " 'cf40a796b1e8775e60a77d410db745012e13410935489c411dbfcadf9d62de19'\n" + "P00 DETAIL: sshfp fingerprint match found for sshfp digest_type [2] hashType [3]" + " 'ded38fadb5713bc6c772e788b5cc41223ca4072c061e5ef152b63ebb1b024096'\n" +#else + "P00 DETAIL: sshfp fingerprint match found for sshfp digest_type [2] hashType [2]" + " 'cf40a796b1e8775e60a77d410db745012e134109'\n" + "P00 DETAIL: sshfp fingerprint match found for sshfp digest_type [2] hashType [2]" + " 'ded38fadb5713bc6c772e788b5cc41223ca4072c'\n" +#endif // LIBSSH2_HOSTKEY_HASH_SHA256 +#ifdef RES_TRUSTAD + "P00 DETAIL: sshfp fingerprint match found for sshfp digest_type [1] hashType [2]" + " '87ac6bede384d2dc6254f396b83ed34856512e64'"); +#else + "P00 DETAIL: sshfp fingerprint match found for sshfp digest_type [1] hashType [2]" + " '87ac6bede384d2dc6254f396b83ed34856512e64'"); +#endif // RES_TRUSTAD + + harnessLogLevelReset(); +#else + TEST_LOG(PROJECT_NAME " not built with sftp support"); +#endif // HAVE_LIBSSH2 + } + + // ***************************************************************************************************************************** + if (testBegin("storageSftpVerifyFingerprint()")) + { +#ifdef HAVE_LIBSSH2 + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpVerifyFingerprint() keyNoMatch - all failures"); + + harnessLogLevelSet(logLevelDetail); + + TimeMSec timeout = 500; + const StorageSftpNewParam param = {.sshfp = true}; + + // Configure a valid host so that we can successfully initialize the resolver + const String *host = STRDEF("www.postgresql.org"); + unsigned int port = 22; + + // Create binary representation of the host keys that will generate all failures - reverse the order of the fingerprints + unsigned char fingerprint4[1024]; + decodeToBin(encodingHex, "BDC1F467AB69238FC4173C20658097835379DBE5", fingerprint4); + unsigned char fingerprint3[1024]; + decodeToBin(encodingHex, "CF40A796B1E8775E60A77D410DB745012E13410935489C411DBFCADF9D62DE19", fingerprint3); + unsigned char fingerprint2[1024]; + decodeToBin(encodingHex, "DED38FADB5713BC6C772E788B5CC41223CA4072C061E5EF152B63EBB1B024096", fingerprint2); + unsigned char fingerprint1[1024]; + decodeToBin(encodingHex, "87ac6bede384d2dc6254f396b83ed34856512e64", fingerprint1); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = 0}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = 0}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = (char *)fingerprint1}, +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[3]", .resultZ = (char *)fingerprint2}, +#else + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = (char *)fingerprint2}, +#endif // LIBSSH2_HOSTKEY_HASH_SHA256 +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[3]", .resultZ = (char *)fingerprint3}, +#else + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = (char *)fingerprint3}, +#endif // LIBSSH2_HOSTKEY_HASH_SHA256 + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = (char *)fingerprint4}, + {.function = NULL}, + }); + + OBJ_NEW_BEGIN(StorageSftp, .childQty = MEM_CONTEXT_QTY_MAX, .callbackQty = 1) + { + *this = (StorageSftp) + { + .interface = storageInterfaceSftp, + .timeout = timeout, + }; + + // Init SFTP session + if (libssh2_init(0) != 0) + THROW_FMT(ServiceError, "unable to init libssh2"); + + this->ioSession = ioClientOpen(sckClientNew(host, port, timeout, timeout)); + this->session = libssh2_session_init(); + + if (this->session == NULL) + THROW_FMT(ServiceError, "unable to init libssh2 session"); + + // Set session to non-blocking + libssh2_session_set_blocking(this->session, 0); + + // Perform handshake + int rc; + + do + { + rc = libssh2_session_handshake(this->session, ioSessionFd(this->ioSession)); + } + while (storageSftpWaitFd(this, rc)); + + if (rc == LIBSSH2_ERROR_EAGAIN) + THROW_FMT(ServiceError, "timeout during libssh2 handshake [%d]", rc); + + if (rc != 0) + THROW_FMT(ServiceError, "libssh2 handshake failed [%d]", rc); + + if (param.sshfp) + { + if (storageSftpResNinit(&my_res_state) != 0) + LOG_WARN("unable to initialize resolver"); + +#ifdef RES_TRUSTAD + // Set the resolver to use TRUSTAD + storageSftpSetOption(&my_res_state, RES_TRUSTAD); +#endif // RES_TRUSTAD + + // Query the server for SSHFP records + unsigned char answer[PACKET_SZ]; + + int len = storageSftpResNquery(&my_res_state, strZ(host), C_IN, T_SSHFP, answer, sizeof(answer)); + + // Check for errors. + // This is dependent on keeping the _DEFAULT_SOURCE for netdb.h. We can drop it and rewrite to a generic error if we + // think that's better. + if (len < 0) + { + LOG_WARN_FMT( + "res_nquery error [%d] %s '%s'", my_res_state.res_h_errno, hstrerror(my_res_state.res_h_errno), strZ(host)); + } + + // Overwrite the sshfp response with a known defined response for testing + Buffer *sshfp = + storageGetP(storageNewReadP(storagePosixNewP(HRN_PATH_REPO_STR), STRDEF("test/data/muffat.debian.org.sshfp"))); + + // Verify we got the expected size + TEST_RESULT_INT((int)bufUsed(sshfp), 195, "expected size"); + + memset(answer, 0, sizeof(answer)); + memmove(answer, bufPtr(sshfp), (size_t)bufUsed(sshfp)); + len = (int)bufUsed(sshfp); + + // Default res_trustad to unset + unsigned char res_trustad = 0; +#ifdef RES_TRUSTAD + // Check the RES_TRUSTAD flag + res_trustad = ((HEADER *)answer)->ad; +#endif // RES_TRUSTAD + + if (res_trustad != 1) + LOG_WARN("Host is untrusted, RES_TRUSTAD not set in response"); + +#ifndef RES_TRUSTAD + LOG_WARN_FMT("RES_TRUSTAD not supported on this OS, host '%s' cannot be verified via sshfp", strZ(host)); +#endif // RES_TRUSTAD + + // Initialize parsing the response + int rc; + ns_msg handle; + if ((rc = storageSftpNsInitparse(answer, len, &handle)) != 0) + LOG_WARN_FMT("ns_initparse error [%d] %s for host '%s'", rc, hstrerror(rc), strZ(host)); + + // Attempt to verify the host via DNS provided fingerprint -- no keys match + TEST_RESULT_BOOL(storageSftpVerifyFingerprint(this->session, handle), false, "no SSHFP record matches for hostkey"); + + // Close the resolver + res_nclose(&my_res_state); + } + } + OBJ_NEW_END(); + + objFree(this); + + TEST_RESULT_LOG( +#ifndef RES_TRUSTAD + "P00 WARN: Host is untrusted, RES_TRUSTAD not set in response\n" + "P00 WARN: RES_TRUSTAD not supported on this OS, host 'www.postgresql.org' cannot be verified via sshfp\n" +#endif // RES_TRUSTAD + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [1] hashType [2]" + " 'bdc1f467ab69238fc4173c20658097835379dbe5'\n" +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [2] hashType [3]" + " 'cf40a796b1e8775e60a77d410db745012e13410935489c411dbfcadf9d62de19'\n" + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [2] hashType [3]" + " 'ded38fadb5713bc6c772e788b5cc41223ca4072c061e5ef152b63ebb1b024096'\n" +#else + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [2] hashType [2]" + " 'cf40a796b1e8775e60a77d410db745012e134109'\n" + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [2] hashType [2]" + " 'ded38fadb5713bc6c772e788b5cc41223ca4072c'\n" +#endif // LIBSSH2_HOSTKEY_HASH_SHA256 +#ifdef RES_TRUSTAD + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [1] hashType [2]" + " '87ac6bede384d2dc6254f396b83ed34856512e64'"); +#else + "P00 DETAIL: no sshfp fingerprint match found for sshfp digest_type [1] hashType [2]" + " '87ac6bede384d2dc6254f396b83ed34856512e64'"); +#endif // RES_TRUSTAD + + harnessLogLevelReset(); +#else + TEST_LOG(PROJECT_NAME " not built with sftp support"); +#endif // HAVE_LIBSSH2 + } + #ifdef HAVE_LIBSSH2 hrnFdReadyShimUninstall(); hrnSckClientOpenShimUninstall(); diff --git a/test/src/module/test/testTest.c b/test/src/module/test/testTest.c index ad9f39cdc6..8b41370b96 100644 --- a/test/src/module/test/testTest.c +++ b/test/src/module/test/testTest.c @@ -323,6 +323,7 @@ testRun(void) " lib_lz4,\n" " lib_pq,\n" " lib_ssh2,\n" + " lib_resolv,\n" " lib_xml,\n" " lib_yaml,\n" " lib_z,\n" @@ -443,6 +444,7 @@ testRun(void) " lib_lz4,\n" " lib_pq,\n" " lib_ssh2,\n" + " lib_resolv,\n" " lib_xml,\n" " lib_yaml,\n" " lib_z,\n" @@ -658,6 +660,7 @@ testRun(void) " lib_lz4,\n" " lib_pq,\n" " lib_ssh2,\n" + " lib_resolv,\n" " lib_xml,\n" " lib_yaml,\n" " lib_z,\n" @@ -831,6 +834,7 @@ testRun(void) " lib_lz4,\n" " lib_pq,\n" " lib_ssh2,\n" + " lib_resolv,\n" " lib_xml,\n" " lib_yaml,\n" " lib_z,\n"