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.")