From c26da096875d609b354ea5989f62af662f9c609e Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa <tatsuhiro.t@gmail.com> Date: Sat, 16 May 2015 19:34:06 +0900 Subject: [PATCH] Add --ssh-host-key-md option Set checksum for SSH host public key. Use same syntax with --checksum option. TYPE is hash type. The supported hash type is sha-1 or md5. DIGEST is hex digest. For example: sha-1=b030503d4de4539dc7885e6f0f5e256704edf4c3. This option can be used to validate server's public key when SFTP is used. If this option is not set, which is default, no validation takes place. --- doc/manual-src/en/aria2c.rst | 10 ++++++++++ src/OptionHandlerFactory.cc | 11 +++++++++++ src/OptionHandlerImpl.cc | 16 ++++++++++++++++ src/OptionHandlerImpl.h | 9 +++++++++ src/SSHSession.cc | 19 ++++++++++++++++++- src/SSHSession.h | 4 ++++ src/SftpNegotiationCommand.cc | 10 +++++++++- src/SftpNegotiationCommand.h | 5 ++++- src/SocketCore.cc | 16 +++++++++++++++- src/SocketCore.h | 2 +- src/prefs.cc | 2 ++ src/prefs.h | 2 ++ src/usage_text.h | 10 ++++++++++ 13 files changed, 111 insertions(+), 5 deletions(-) diff --git a/doc/manual-src/en/aria2c.rst b/doc/manual-src/en/aria2c.rst index c9da0644..280e454e 100644 --- a/doc/manual-src/en/aria2c.rst +++ b/doc/manual-src/en/aria2c.rst @@ -592,6 +592,15 @@ FTP/SFTP Specific Options Reuse connection in FTP. Default: ``true`` +.. option:: --ssh-host-key-md=<TYPE>=<DIGEST> + + Set checksum for SSH host public key. TYPE is hash type. The + supported hash type is ``sha-1`` or ``md5``. DIGEST is hex + digest. For example: + ``sha-1=b030503d4de4539dc7885e6f0f5e256704edf4c3``. This option can + be used to validate server's public key when SFTP is used. If this + option is not set, which is default, no validation takes place. + BitTorrent/Metalink Options ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. option:: --select-file=<INDEX>... @@ -2030,6 +2039,7 @@ of URIs. These optional lines must start with white space(s). * :option:`seed-time <--seed-time>` * :option:`select-file <--select-file>` * :option:`split <-s>` + * :option:`ssh-host-key-md <--ssh-host-key-md>` * :option:`stream-piece-selector <--stream-piece-selector>` * :option:`timeout <-t>` * :option:`uri-selector <--uri-selector>` diff --git a/src/OptionHandlerFactory.cc b/src/OptionHandlerFactory.cc index 1e368100..768d8471 100644 --- a/src/OptionHandlerFactory.cc +++ b/src/OptionHandlerFactory.cc @@ -1506,6 +1506,17 @@ std::vector<OptionHandler*> OptionHandlerFactory::createOptionHandlers() op->setChangeOptionForReserved(true); handlers.push_back(op); } + { + OptionHandler* op(new ChecksumOptionHandler + (PREF_SSH_HOST_KEY_MD, + TEXT_SSH_HOST_KEY_MD, + {"sha-1", "md5"})); + op->addTag(TAG_FTP); + op->setInitialOption(true); + op->setChangeGlobalOption(true); + op->setChangeOptionForReserved(true); + handlers.push_back(op); + } { OptionHandler* op(new DefaultOptionHandler (PREF_NETRC_PATH, diff --git a/src/OptionHandlerImpl.cc b/src/OptionHandlerImpl.cc index adf751f8..f0771d8c 100644 --- a/src/OptionHandlerImpl.cc +++ b/src/OptionHandlerImpl.cc @@ -373,6 +373,16 @@ ChecksumOptionHandler::ChecksumOptionHandler OptionHandler::REQ_ARG, shortName) {} +ChecksumOptionHandler::ChecksumOptionHandler +(PrefPtr pref, + const char* description, + std::vector<std::string> acceptableTypes, + char shortName) + : AbstractOptionHandler(pref, description, NO_DEFAULT_VALUE, + OptionHandler::REQ_ARG, shortName), + acceptableTypes_(std::move(acceptableTypes)) +{} + ChecksumOptionHandler::~ChecksumOptionHandler() {} void ChecksumOptionHandler::parseArg(Option& option, const std::string& optarg) @@ -380,6 +390,12 @@ void ChecksumOptionHandler::parseArg(Option& option, const std::string& optarg) { auto p = util::divide(std::begin(optarg), std::end(optarg), '='); std::string hashType(p.first.first, p.first.second); + if(!acceptableTypes_.empty() && + std::find(std::begin(acceptableTypes_), std::end(acceptableTypes_), + hashType) == std::end(acceptableTypes_)) { + throw DL_ABORT_EX(fmt("Checksum type %s is not acceptable", + hashType.c_str())); + } std::string hexDigest(p.second.first, p.second.second); util::lowercase(hashType); util::lowercase(hexDigest); diff --git a/src/OptionHandlerImpl.h b/src/OptionHandlerImpl.h index faea9a27..7dc0295c 100644 --- a/src/OptionHandlerImpl.h +++ b/src/OptionHandlerImpl.h @@ -177,10 +177,19 @@ public: ChecksumOptionHandler(PrefPtr pref, const char* description, char shortName = 0); + ChecksumOptionHandler(PrefPtr pref, + const char* description, + std::vector<std::string> acceptableTypes, + char shortName = 0); virtual ~ChecksumOptionHandler(); virtual void parseArg(Option& option, const std::string& optarg) const CXX11_OVERRIDE; virtual std::string createPossibleValuesString() const CXX11_OVERRIDE; + +private: + // message digest type acceptable for this option. Empty means that + // it accepts all supported types. + std::vector<std::string> acceptableTypes_; }; class ParameterOptionHandler : public AbstractOptionHandler { diff --git a/src/SSHSession.cc b/src/SSHSession.cc index 94abcece..1ee3c07d 100644 --- a/src/SSHSession.cc +++ b/src/SSHSession.cc @@ -36,6 +36,8 @@ #include <cassert> +#include "MessageDigest.h" + namespace aria2 { SSHSession::SSHSession() @@ -172,10 +174,25 @@ int SSHSession::handshake() if (rv != 0) { return SSH_ERR_ERROR; } - // TODO we have to validate server's fingerprint return SSH_ERR_OK; } +std::string SSHSession::hostkeyMessageDigest(const std::string& hashType) { + int h; + if (hashType == "sha-1") { + h = LIBSSH2_HOSTKEY_HASH_SHA1; + } else if (hashType == "md5") { + h = LIBSSH2_HOSTKEY_HASH_MD5; + } else { + return ""; + } + auto fingerprint = libssh2_hostkey_hash(ssh2_, h); + if (!fingerprint) { + return ""; + } + return std::string(fingerprint, MessageDigest::getDigestLength(hashType)); +} + int SSHSession::authPassword(const std::string& user, const std::string& password) { diff --git a/src/SSHSession.h b/src/SSHSession.h index 4e19c5df..87234eee 100644 --- a/src/SSHSession.h +++ b/src/SSHSession.h @@ -100,6 +100,10 @@ public: // blocks, or SSH_ERR_ERROR. int handshake(); + // Returns message digest of host's public key. |hashType| must be + // either "sha-1" or "md5". + std::string hostkeyMessageDigest(const std::string& hashType); + // Performs authentication using username and password. This // function returns SSH_ERR_OK if it succeeds, or SSH_ERR_WOULDBLOCK // if the underlying transport blocks, or SSH_ERR_ERROR. diff --git a/src/SftpNegotiationCommand.cc b/src/SftpNegotiationCommand.cc index ee836cf1..2bfdad96 100644 --- a/src/SftpNegotiationCommand.cc +++ b/src/SftpNegotiationCommand.cc @@ -82,6 +82,14 @@ SftpNegotiationCommand::SftpNegotiationCommand { path_ = getPath(); setWriteCheckSocket(getSocket()); + + const std::string& checksum = getOption()->get(PREF_SSH_HOST_KEY_MD); + if (!checksum.empty()) { + auto p = util::divide(std::begin(checksum), std::end(checksum), '='); + hashType_.assign(p.first.first, p.first.second); + util::lowercase(hashType_); + digest_ = util::fromHex(p.second.first, p.second.second); + } } SftpNegotiationCommand::~SftpNegotiationCommand() {} @@ -92,7 +100,7 @@ bool SftpNegotiationCommand::executeInternal() { switch(sequence_) { case SEQ_HANDSHAKE: setReadCheckSocket(getSocket()); - if (!getSocket()->sshHandshake()) { + if (!getSocket()->sshHandshake(hashType_, digest_)) { goto again; } A2_LOG_DEBUG(fmt("CUID#%" PRId64 " - SSH handshake success", getCuid())); diff --git a/src/SftpNegotiationCommand.h b/src/SftpNegotiationCommand.h index a6d1045c..3ce11d40 100644 --- a/src/SftpNegotiationCommand.h +++ b/src/SftpNegotiationCommand.h @@ -68,7 +68,10 @@ private: std::unique_ptr<AuthConfig> authConfig_; // remote file path std::string path_; - + // expected host's public key message digest: hash type and digest + // (raw binary value). + std::string hashType_; + std::string digest_; protected: virtual bool executeInternal() CXX11_OVERRIDE; diff --git a/src/SocketCore.cc b/src/SocketCore.cc index 320f04a6..701fc33e 100644 --- a/src/SocketCore.cc +++ b/src/SocketCore.cc @@ -989,7 +989,8 @@ bool SocketCore::tlsHandshake(TLSContext* tlsctx, const std::string& hostname) #ifdef HAVE_LIBSSH2 -bool SocketCore::sshHandshake() +bool SocketCore::sshHandshake(const std::string& hashType, + const std::string& digest) { wantRead_ = false; wantWrite_ = false; @@ -1009,6 +1010,19 @@ bool SocketCore::sshHandshake() throw DL_ABORT_EX(fmt("SSH handshake failure: %s", sshSession_->getLastErrorString().c_str())); } + if (!hashType.empty()) { + auto actualDigest = sshSession_->hostkeyMessageDigest(hashType); + if (actualDigest.empty()) { + throw DL_ABORT_EX(fmt("Empty host key fingerprint from SSH layer: " + "perhaps hash type %s is not supported?", + hashType.c_str())); + } + if (digest != actualDigest) { + throw DL_ABORT_EX(fmt("Unexpected SSH host key: expected %s, actual %s", + util::toHex(digest).c_str(), + util::toHex(actualDigest).c_str())); + } + } return true; } diff --git a/src/SocketCore.h b/src/SocketCore.h index 065e120f..bc6d1daa 100644 --- a/src/SocketCore.h +++ b/src/SocketCore.h @@ -302,7 +302,7 @@ public: #ifdef HAVE_LIBSSH2 // Performs SSH handshake - bool sshHandshake(); + bool sshHandshake(const std::string& hashType, const std::string& digest); // Performs SSH authentication using username and password. bool sshAuthPassword(const std::string& user, const std::string& password); // Starts sftp session and open remote file |path|. diff --git a/src/prefs.cc b/src/prefs.cc index 903e9d5f..a3046b54 100644 --- a/src/prefs.cc +++ b/src/prefs.cc @@ -387,6 +387,8 @@ PrefPtr PREF_FTP_TYPE = makePref("ftp-type"); PrefPtr PREF_FTP_PASV = makePref("ftp-pasv"); // values: true | false PrefPtr PREF_FTP_REUSE_CONNECTION = makePref("ftp-reuse-connection"); +// values: hashType=digest +PrefPtr PREF_SSH_HOST_KEY_MD = makePref("ssh-host-key-md"); /** * HTTP related preferences diff --git a/src/prefs.h b/src/prefs.h index 2b3ade6d..66eda2fb 100644 --- a/src/prefs.h +++ b/src/prefs.h @@ -324,6 +324,8 @@ extern PrefPtr PREF_FTP_TYPE; extern PrefPtr PREF_FTP_PASV; // values: true | false extern PrefPtr PREF_FTP_REUSE_CONNECTION; +// values: hashType=digest +extern PrefPtr PREF_SSH_HOST_KEY_MD; /** * HTTP related preferences diff --git a/src/usage_text.h b/src/usage_text.h index 64f64a43..d595c233 100644 --- a/src/usage_text.h +++ b/src/usage_text.h @@ -1030,3 +1030,13 @@ " If true is given, deny legacy BitTorrent\n" \ " handshake and only use Obfuscation handshake and\n" \ " always encrypt message payload.") +#define TEXT_SSH_HOST_KEY_MD \ + _(" --ssh-host-key-md=TYPE=DIGEST\n" \ + " Set checksum for SSH host public key. TYPE is\n" \ + " hash type. The supported hash type is sha-1 or\n" \ + " md5. DIGEST is hex digest. For example:\n" \ + " sha-1=b030503d4de4539dc7885e6f0f5e256704edf4c3\n" \ + " This option can be used to validate server's\n" \ + " public key when SFTP is used. If this option is\n" \ + " not set, which is default, no validation takes\n" \ + " place.")