소스 검색

Internally use HMAC in http auth

To at least get constant time compare.
Also fix incorrect parsing of the creds (were incorrectly stripped).
Also add unit tests.
Nils Maier 11 년 전
부모
커밋
f7cc24d6cf
5개의 변경된 파일240개의 추가작업 그리고 15개의 파일을 삭제
  1. 29 8
      src/HttpServer.cc
  2. 11 2
      src/HttpServer.h
  3. 11 5
      src/util.h
  4. 188 0
      test/HttpServerTest.cc
  5. 1 0
      test/Makefile.am

+ 29 - 8
src/HttpServer.cc

@@ -42,6 +42,7 @@
 #include "DlAbortEx.h"
 #include "message.h"
 #include "util.h"
+#include "util_security.h"
 #include "LogFactory.h"
 #include "Logger.h"
 #include "base64.h"
@@ -57,6 +58,8 @@
 
 namespace aria2 {
 
+std::unique_ptr<util::security::HMAC> HttpServer::hmac_;
+
 HttpServer::HttpServer(const std::shared_ptr<SocketCore>& socket)
  : socket_(socket),
    socketRecvBuffer_(new SocketRecvBuffer(socket_)),
@@ -279,7 +282,7 @@ bool HttpServer::sendBufferIsEmpty() const
 
 bool HttpServer::authenticate()
 {
-  if(username_.empty()) {
+  if (!username_) {
     return true;
   }
 
@@ -292,19 +295,37 @@ bool HttpServer::authenticate()
   if(!util::streq(p.first.first, p.first.second, "Basic")) {
     return false;
   }
+
   std::string userpass = base64::decode(p.second.first, p.second.second);
-  auto up = util::divide(std::begin(userpass), std::end(userpass), ':');
-  return util::streq(up.first.first, up.first.second,
-                     username_.begin(), username_.end()) &&
-    util::streq(up.second.first, up.second.second,
-                password_.begin(), password_.end());
+  auto up = util::divide(std::begin(userpass), std::end(userpass), ':', false);
+  std::string username(up.first.first, up.first.second);
+  std::string password(up.second.first, up.second.second);
+  return *username_ == hmac_->getResult(username) &&
+    (!password_ || *password_ == hmac_->getResult(password));
 }
 
 void HttpServer::setUsernamePassword
 (const std::string& username, const std::string& password)
 {
-  username_ = username;
-  password_ = password;
+  using namespace util::security;
+
+  if (!hmac_) {
+    hmac_ = HMAC::createRandom();
+  }
+
+  if (!username.empty()) {
+    username_ = make_unique<HMACResult>(hmac_->getResult(username));
+  }
+  else {
+    username_.reset();
+  }
+
+  if (!password.empty()) {
+    password_ = make_unique<HMACResult>(hmac_->getResult(password));
+  }
+  else {
+    password_.reset();
+  }
 }
 
 int HttpServer::setupResponseRecv()

+ 11 - 2
src/HttpServer.h

@@ -53,6 +53,13 @@ class DownloadEngine;
 class SocketRecvBuffer;
 class DiskWriter;
 
+namespace util {
+  namespace security {
+    class HMAC;
+    class HMACResult;
+  }
+}
+
 enum RequestType {
   RPC_TYPE_NONE,
   RPC_TYPE_XML,
@@ -64,6 +71,8 @@ enum RequestType {
 // intended to be a generic HTTP server.
 class HttpServer {
 private:
+  static std::unique_ptr<util::security::HMAC> hmac_;
+
   std::shared_ptr<SocketCore> socket_;
   std::shared_ptr<SocketRecvBuffer> socketRecvBuffer_;
   SocketBuffer socketBuffer_;
@@ -77,8 +86,8 @@ private:
   std::unique_ptr<DiskWriter> lastBody_;
   bool keepAlive_;
   bool gzip_;
-  std::string username_;
-  std::string password_;
+  std::unique_ptr<util::security::HMACResult> username_;
+  std::unique_ptr<util::security::HMACResult> password_;
   bool acceptsGZip_;
   std::string allowOrigin_;
   bool secure_;

+ 11 - 5
src/util.h

@@ -156,14 +156,20 @@ std::string strip
 template<typename InputIterator>
 std::pair<std::pair<InputIterator, InputIterator>,
           std::pair<InputIterator, InputIterator>>
-divide(InputIterator first, InputIterator last, char delim)
+divide(InputIterator first, InputIterator last, char delim, bool strip=true)
 {
   auto dpos = std::find(first, last, delim);
-  if(dpos == last) {
-    return {stripIter(first, last), {last, last}};
-  } else {
-    return {stripIter(first, dpos), stripIter(dpos+1, last)};
+  if (dpos == last) {
+    if (strip) {
+      return {stripIter(first, last), {last, last}};
+    }
+    return {{first, last}, {last, last}};
+  }
+
+  if (strip) {
+    return {stripIter(first, dpos), stripIter(dpos + 1, last)};
   }
+  return {{first, dpos}, {dpos + 1, last}};
 }
 
 template<typename T>

+ 188 - 0
test/HttpServerTest.cc

@@ -0,0 +1,188 @@
+#include "HttpServer.h"
+
+#include <cppunit/extensions/HelperMacros.h>
+
+#include "SocketCore.h"
+#include "a2functional.h"
+
+namespace aria2 {
+
+class HttpServerTest : public CppUnit::TestFixture
+{
+  CPPUNIT_TEST_SUITE(HttpServerTest);
+  CPPUNIT_TEST(testHttpBasicAuth);
+  CPPUNIT_TEST_SUITE_END();
+public:
+  void testHttpBasicAuth();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(HttpServerTest);
+
+namespace {
+  std::unique_ptr<HttpServer> performHttpRequest(SocketCore& server, std::string request)
+  {
+    std::pair<std::string, uint16_t> addr;
+    server.getAddrInfo(addr);
+
+    SocketCore client;
+    client.establishConnection("localhost", addr.second);
+    while (!client.isWritable(0)) {}
+
+    auto inbound = server.acceptConnection();
+    inbound->setBlockingMode();
+    auto rv = make_unique<HttpServer>(inbound);
+
+    client.writeData(request);
+    while (!rv->receiveRequest()) {}
+    return rv;
+  }
+} // namespace
+
+void HttpServerTest::testHttpBasicAuth()
+{
+  SocketCore server;
+  server.bind(0);
+  server.beginListen();
+  server.setBlockingMode();
+
+  {
+    // Default is no auth
+    auto req = performHttpRequest(
+        server, "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\n\r\n");
+    CPPUNIT_ASSERT(req->authenticate());
+  }
+
+  {
+    // Empty user-name and password should come out as no auth.
+    auto req = performHttpRequest(
+        server, "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\n\r\n");
+    req->setUsernamePassword("", "");
+    CPPUNIT_ASSERT(req->authenticate());
+  }
+
+  {
+    // Empty user-name but set password should also come out as no auth.
+    auto req = performHttpRequest(
+        server, "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\n\r\n");
+    req->setUsernamePassword("", "pass");
+    CPPUNIT_ASSERT(req->authenticate());
+  }
+
+  {
+    // Client provided credentials should be ignored when there is no auth.
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\nAuthorization: Basic dXNlcjpwYXNz\r\n\r\n");
+    req->setUsernamePassword("", "pass");
+    CPPUNIT_ASSERT(req->authenticate());
+  }
+
+  {
+    // Correct client provided credentials should match.
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\nAuthorization: Basic dXNlcjpwYXNz\r\n\r\n");
+    req->setUsernamePassword("user", "pass");
+    CPPUNIT_ASSERT(req->authenticate());
+  }
+
+  {
+    // Correct client provided credentials should match (2).
+    // Embedded nulls
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\nAuthorization: Basic dXNlcgBudWxsOnBhc3MAbnVsbA==\r\n\r\n");
+    req->setUsernamePassword(std::string("user\0null", 9), std::string("pass\0null", 9));
+    CPPUNIT_ASSERT(req->authenticate());
+  }
+
+  {
+    // Correct client provided credentials should match (3).
+    // Embedded, leading nulls
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\nAuthorization: Basic AHVzZXI6AHBhc3M=\r\n\r\n");
+    req->setUsernamePassword(std::string("\0user", 5), std::string("\0pass", 5));
+    CPPUNIT_ASSERT(req->authenticate());
+  }
+
+  {
+    // Correct client provided credentials should match (3).
+    // Whitespace
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\nAuthorization: Basic IHVzZXIJOgpwYXNzDQ==\r\n\r\n");
+    req->setUsernamePassword(" user\t", "\npass\r");
+    CPPUNIT_ASSERT(req->authenticate());
+  }
+
+  {
+    // Wrong client provided credentials should NOT match.
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\nAuthorization: Basic dXNlcjpwYXNz\r\n\r\n");
+    req->setUsernamePassword("user", "pass2");
+    CPPUNIT_ASSERT(!req->authenticate());
+  }
+
+  {
+    // Wrong client provided credentials should NOT match (2).
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\nAuthorization: Basic dXNlcjpwYXNz\r\n\r\n");
+    req->setUsernamePassword("user2", "pass");
+    CPPUNIT_ASSERT(!req->authenticate());
+  }
+
+  {
+    // Wrong client provided credentials should NOT match (3).
+    // Embedded null in pass config.
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\nAuthorization: Basic dXNlcjpwYXNz\r\n\r\n");
+    req->setUsernamePassword("user", std::string("pass\0three", 10));
+    CPPUNIT_ASSERT(!req->authenticate());
+  }
+
+  {
+    // Wrong client provided credentials should NOT match (4).
+    // Embedded null in user config.
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\nAuthorization: Basic dXNlcjpwYXNz\r\n\r\n");
+    req->setUsernamePassword(std::string("user\0four", 9), "pass");
+    CPPUNIT_ASSERT(!req->authenticate());
+  }
+
+  {
+    // Wrong client provided credentials should NOT match (5).
+    // Embedded null in http auth.
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\nAuthorization: Basic dXNlcjpwYXNzAHRocmVl\r\n\r\n");
+    req->setUsernamePassword("user", "pass");
+    CPPUNIT_ASSERT(!req->authenticate());
+  }
+
+  {
+    // Wrong client provided credentials should NOT match (6).
+    // Embedded null in http auth.
+    // Embedded, leading nulls
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\nAuthorization: Basic AHVzZXI6AHBhc3M=\r\n\r\n");
+    req->setUsernamePassword(std::string("\0user5", 6), std::string("\0pass", 5));
+    CPPUNIT_ASSERT(!req->authenticate());
+  }
+
+  {
+    // When there is a user and password, the client must actually provide auth.
+    auto req = performHttpRequest(
+        server,
+        "GET / HTTP/1.1\r\nUser-Agent: aria2-test\r\n\r\n");
+    req->setUsernamePassword("user", "pass");
+    CPPUNIT_ASSERT(!req->authenticate());
+  }
+}
+
+} // namespace aria2

+ 1 - 0
test/Makefile.am

@@ -77,6 +77,7 @@ aria2c_SOURCES = AllTest.cc\
 	ValueBaseJsonParserTest.cc\
 	RpcResponseTest.cc\
 	RpcMethodTest.cc\
+	HttpServerTest.cc\
 	BufferedFileTest.cc\
 	GeomStreamPieceSelectorTest.cc\
 	SegListTest.cc\