]> granicus.if.org Git - icinga2/commitdiff
Hash API password and comparison
authorJean Flach <jean-marcel.flach@icinga.com>
Fri, 11 Aug 2017 14:23:24 +0000 (16:23 +0200)
committerGunnar Beutner <gunnar.beutner@icinga.com>
Thu, 15 Feb 2018 12:09:22 +0000 (13:09 +0100)
fixes #4920

15 files changed:
doc/09-object-types.md
doc/11-cli-commands.md
doc/12-icinga2-api.md
lib/base/tlsutility.cpp
lib/base/tlsutility.hpp
lib/cli/CMakeLists.txt
lib/cli/apisetupcommand.cpp
lib/cli/apiusercommand.cpp [new file with mode: 0644]
lib/cli/apiusercommand.hpp [new file with mode: 0644]
lib/remote/apiuser.cpp
lib/remote/apiuser.hpp
lib/remote/apiuser.ti
lib/remote/httpserverconnection.cpp
test/CMakeLists.txt
test/remote-user.cpp [new file with mode: 0644]

index b0894b62b057ba080ce0ea1d80e36b684e71341e..b846964db2d104b18f62bd2182e824b8934a7f61 100644 (file)
@@ -108,8 +108,9 @@ Configuration Attributes:
   Name                      | Type                  | Description
   --------------------------|-----------------------|----------------------------------
   password                  | String                | **Optional.** Password string. Note: This attribute is hidden in API responses.
+  hashed\_password          | String                | **Optional.** A hashed password string in the form of /etc/shadow. Note: This attribute is hidden in API responses.
   client\_cn                | String                | **Optional.** Client Common Name (CN).
-  permissions              | Array                 | **Required.** Array of permissions. Either as string or dictionary with the keys `permission` and `filter`. The latter must be specified as function.
+  permissions               | Array                 | **Required.** Array of permissions. Either as string or dictionary with the keys `permission` and `filter`. The latter must be specified as function.
 
 Available permissions are explained in the [API permissions](12-icinga2-api.md#icinga2-api-permissions)
 chapter.
index c5e9d289afe1b2b7b94382a84e3e2a31f7bf4abf..10acf727ac7e3634c6140158154f29ede518b409 100644 (file)
@@ -19,7 +19,8 @@ Usage:
   icinga2 <command> [<arguments>]
 
 Supported commands:
-  * api setup (setup for api)
+  * api setup (setup for API)
+  * api user (API user creation helper)
   * ca list (lists all certificate signing requests)
   * ca sign (signs an outstanding certificate request)
   * console (Icinga console)
@@ -135,8 +136,9 @@ added.
 
 ## CLI command: Api <a id="cli-command-api"></a>
 
-Provides the setup CLI command to enable the REST API. More details
-in the [Icinga 2 API](12-icinga2-api.md#icinga2-api-setup) chapter.
+Provides the helper functions `api setup` and `api user`. The first to enable the REST API, the second to create
+ApiUser objects with hashed password strings.
+More details in the [Icinga 2 API](12-icinga2-api.md#icinga2-api-setup) chapter.
 
 ```
 # icinga2 api --help
@@ -146,7 +148,8 @@ Usage:
   icinga2 <command> [<arguments>]
 
 Supported commands:
-  * api setup (setup for api)
+  * api setup (setup for API)
+  * api user (API user creation helper)
 
 Global options:
   -h [ --help ]             show this help message
index 58aaf6873faba3ae78a33e61f8ef0e34257d544c..b5a883c9085e049d14f9fad2efa8c9505f7561a1 100644 (file)
@@ -21,6 +21,25 @@ If you prefer to set up the API manually, you will have to perform the following
 
 The next chapter provides a quick overview of how you can use the API.
 
+### Creating ApiUsers
+
+The CLI command `icinga2 api user` allows you to create an ApiUser object with a hashed password string, ready to be
+added to your configuration. Example:
+
+```
+$ icinga2 api user --user icingaweb2 --passwd icinga
+object ApiUser "icingaweb2" {
+  password_hash ="$5$d5f1a17ea308acb6$9e9fd5d24a9373a16e8811765cc5a5939687faf9ef8ed496db6e7f1d0ae9b2a9"
+  // client_cn = ""
+
+  permissions = [ "*" ]
+}
+```
+
+Optionally a salt can be provided with `--salt`, otherwise a random value will be used. When ApiUsers are stored this
+way, even somebody able to read the configuration files won't be able to authenticate using this information. There is
+no way to recover your password should you forget it, you'd need to create it anew.
+
 ## Introduction <a id="icinga2-api-introduction"></a>
 
 The Icinga 2 API allows you to manage configuration objects
index 3955ceca481278627ebb4700f969de6887a3c6d2..1f74a2735a27b09fdf63aae743f552a12bafd987 100644 (file)
@@ -658,6 +658,19 @@ String PBKDF2_SHA1(const String& password, const String& salt, int iterations)
        return output;
 }
 
+String PBKDF2_SHA256(const String& password, const String& salt, int iterations)
+{
+       unsigned char digest[SHA256_DIGEST_LENGTH];
+       PKCS5_PBKDF2_HMAC(password.CStr(), password.GetLength(), reinterpret_cast<const unsigned char *>(salt.CStr()),
+               salt.GetLength(), iterations, EVP_sha256(), SHA256_DIGEST_LENGTH, digest);
+
+       char output[SHA256_DIGEST_LENGTH*2+1];
+       for (int i = 0; i < SHA256_DIGEST_LENGTH; i++)
+               sprintf(output + 2 * i, "%02x", digest[i]);
+
+       return output;
+}
+
 String SHA1(const String& s, bool binary)
 {
        char errbuf[120];
index 7928c56d1ac004d41728d4b261073bd5318de5a5..38df7e58756c53e4b10f6dfb389a8cde2ec41cda 100644 (file)
@@ -51,6 +51,7 @@ std::shared_ptr<X509> StringToCertificate(const String& cert);
 std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject);
 std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert);
 String PBKDF2_SHA1(const String& password, const String& salt, int iterations);
+String PBKDF2_SHA256(const String& password, const String& salt, int iterations);
 String SHA1(const String& s, bool binary = false);
 String SHA256(const String& s);
 String RandomString(int length);
index bb2c75f4734e1fdf1b7df753a1edcb49e258365f..7a147f82ff67a1b4609614c7c782cd02163344ac 100644 (file)
@@ -18,6 +18,7 @@
 set(cli_SOURCES
   i2-cli.hpp
   apisetupcommand.cpp apisetupcommand.hpp
+  apiusercommand.cpp apiusercommand.hpp
   apisetuputility.cpp apisetuputility.hpp
   calistcommand.cpp calistcommand.hpp
   casigncommand.cpp casigncommand.hpp
index 1345dc92b45d145abced5143f8594fa73c697fbb..bc832c52551fad255b43afe2a56f1999c161fddc 100644 (file)
@@ -36,7 +36,7 @@ String ApiSetupCommand::GetDescription() const
 
 String ApiSetupCommand::GetShortDescription() const
 {
-       return "setup for api";
+       return "setup for API";
 }
 
 ImpersonationLevel ApiSetupCommand::GetImpersonationLevel() const
diff --git a/lib/cli/apiusercommand.cpp b/lib/cli/apiusercommand.cpp
new file mode 100644 (file)
index 0000000..9d43e12
--- /dev/null
@@ -0,0 +1,82 @@
+/******************************************************************************
+ * Icinga 2                                                                   *
+ * Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/)  *
+ *                                                                            *
+ * This program is free software; you can redistribute it and/or              *
+ * modify it under the terms of the GNU General Public License                *
+ * as published by the Free Software Foundation; either version 2             *
+ * of the License, or (at your option) any later version.                     *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program; if not, write to the Free Software Foundation     *
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.             *
+ ******************************************************************************/
+
+#include "cli/apiusercommand.hpp"
+#include "base/logger.hpp"
+#include "base/tlsutility.hpp"
+#include "remote/apiuser.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("api/user", ApiUserCommand);
+
+String ApiUserCommand::GetDescription(void) const
+{
+       return "Create a hashed user and password string for the Icinga 2 API";
+}
+
+String ApiUserCommand::GetShortDescription(void) const
+{
+       return "API user creation helper";
+}
+
+void ApiUserCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+    boost::program_options::options_description& hiddenDesc) const
+{
+       visibleDesc.add_options()
+               ("user", po::value<std::string>(), "API username")
+               ("passwd", po::value<std::string>(), "Password in clear text")
+               ("salt", po::value<std::string>(), "Optional salt (default: 8 random chars)");
+}
+
+/**
+ * The entry point for the "api user" CLI command.
+ *
+ * @returns An exit status.
+ */
+int ApiUserCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+       if (!vm.count("user")) {
+               Log(LogCritical, "cli", "Username (--user) must be specified.");
+               return 1;
+       }
+
+       if (!vm.count("passwd")) {
+               Log(LogCritical, "cli", "Password (--passwd) must be specified.");
+               return 1;
+       }
+
+       String user = vm["user"].as<std::string>();
+       String passwd = vm["passwd"].as<std::string>();
+       String salt = vm.count("salt") ? String(vm["salt"].as<std::string>()) : RandomString(8);
+
+       String hashedPassword = ApiUser::CreateHashedPasswordString(passwd, salt, true);
+
+       std::cout
+               << "object ApiUser \"" << user << "\" {\n"
+               << "  password_hash =\"" << hashedPassword << "\"\n"
+               << "  // client_cn = \"\"\n"
+               << "\n"
+               << "  permissions = [ \"*\" ]\n"
+               << "}\n";
+
+       return 0;
+}
diff --git a/lib/cli/apiusercommand.hpp b/lib/cli/apiusercommand.hpp
new file mode 100644 (file)
index 0000000..4a4bfb2
--- /dev/null
@@ -0,0 +1,47 @@
+/******************************************************************************
+ * Icinga 2                                                                   *
+ * Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/)  *
+ *                                                                            *
+ * This program is free software; you can redistribute it and/or              *
+ * modify it under the terms of the GNU General Public License                *
+ * as published by the Free Software Foundation; either version 2             *
+ * of the License, or (at your option) any later version.                     *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program; if not, write to the Free Software Foundation     *
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.             *
+ ******************************************************************************/
+
+#ifndef APIUSERCOMMAND_H
+#define APIUSERCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "api user" command.
+ *
+ * @ingroup cli
+ */
+class ApiUserCommand : public CLICommand
+{
+public:
+       DECLARE_PTR_TYPEDEFS(ApiUserCommand);
+
+       virtual String GetDescription(void) const override;
+       virtual String GetShortDescription(void) const override;
+       virtual void InitParameters(boost::program_options::options_description& visibleDesc,
+               boost::program_options::options_description& hiddenDesc) const override;
+       virtual int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* APIUSERCOMMAND_H */
index 183291af314db4ee16fdb8bd4c96e26f82091277..ea3eb78497c55e9cf61a1509b5c0f74a95f54853 100644 (file)
 #include "remote/apiuser.hpp"
 #include "remote/apiuser-ti.cpp"
 #include "base/configtype.hpp"
+#include "base/tlsutility.hpp"
 
 using namespace icinga;
 
 REGISTER_TYPE(ApiUser);
 
+void ApiUser::OnConfigLoaded(void)
+{
+       ObjectImpl<ApiUser>::OnConfigLoaded();
+
+       if (this->GetPasswordHash().IsEmpty())
+               SetPasswordHash(CreateHashedPasswordString(GetPassword(), RandomString(8), true));
+}
+
 ApiUser::Ptr ApiUser::GetByClientCN(const String& cn)
 {
        for (const ApiUser::Ptr& user : ConfigType::GetObjectsByType<ApiUser>()) {
@@ -34,3 +43,50 @@ ApiUser::Ptr ApiUser::GetByClientCN(const String& cn)
 
        return nullptr;
 }
+
+bool ApiUser::ComparePassword(String password) const
+{
+       Dictionary::Ptr passwordDict = this->GetPasswordDict();
+       String thisPassword = passwordDict->Get("password");
+       String otherPassword = CreateHashedPasswordString(password, passwordDict->Get("salt"), false);
+
+       const char *p1 = otherPassword.CStr();
+       const char *p2 = thisPassword.CStr();
+
+       volatile char c = 0;
+
+       for (size_t i=0; i<64; ++i)
+               c |= p1[i] ^ p2[i];
+
+       return (c == 0);
+}
+
+Dictionary::Ptr ApiUser::GetPasswordDict(void) const
+{
+       String password = this->GetPasswordHash();
+       if (password.IsEmpty() || password[0] != '$')
+               return nullptr;
+
+       String::SizeType saltBegin = password.FindFirstOf('$', 1);
+       String::SizeType passwordBegin = password.FindFirstOf('$', saltBegin+1);
+
+       if (saltBegin == String::NPos || saltBegin == 1 || passwordBegin == String::NPos)
+               return nullptr;
+
+       Dictionary::Ptr passwordDict = new Dictionary();
+       passwordDict->Set("algorithm", password.SubStr(1, saltBegin - 1));
+       passwordDict->Set("salt", password.SubStr(saltBegin + 1, passwordBegin - saltBegin - 1));
+       passwordDict->Set("password", password.SubStr(passwordBegin + 1));
+
+       return passwordDict;
+}
+
+String ApiUser::CreateHashedPasswordString(const String& password, const String& salt, const bool shadow)
+{
+       if (shadow)
+               //Using /etc/shadow password format. The 5 means SHA256 is being used
+               return String("$5$" + salt + "$" + PBKDF2_SHA256(password, salt, 1000));
+       else
+               return PBKDF2_SHA256(password, salt, 1000);
+
+}
index 755273bf4cf46f86e8c6b9399e1f0c051847e7db..eeae3c77bcf5b1d7e5ae2c04cc4688891add8835 100644 (file)
@@ -35,7 +35,13 @@ public:
        DECLARE_OBJECT(ApiUser);
        DECLARE_OBJECTNAME(ApiUser);
 
+       virtual void OnConfigLoaded(void) override;
+
        static ApiUser::Ptr GetByClientCN(const String& cn);
+       static String CreateHashedPasswordString(const String& password, const String& salt, const bool shadow = false);
+
+       Dictionary::Ptr GetPasswordDict(void) const;
+       bool ComparePassword(String password) const;
 };
 
 }
index aef71fd4f7009d0a4c7cff8774ffb9d1ee8369fd..a767f833a09dd96b0f02bceb7d6dd44f01f3ae1f 100644 (file)
@@ -27,7 +27,9 @@ namespace icinga
 
 class ApiUser : ConfigObject
 {
-       [config, no_user_view] String password;
+       /* No show config */
+       [no_user_view, no_user_modify] String password;
+       [config, no_user_view] String password_hash;
        [config] String client_cn (ClientCN);
        [config] array(Value) permissions;
 };
index 1b19635a419904c99fc841e077279dda516bcb59..4fbd774b725e49355c57232d83a22ee93796f572 100644 (file)
@@ -157,9 +157,7 @@ void HttpServerConnection::ProcessMessageAsync(HttpRequest& request)
                user = ApiUser::GetByName(username);
 
                /* Deny authentication if 1) given password is empty 2) configured password does not match. */
-               if (password.IsEmpty())
-                       user.reset();
-               else if (user && user->GetPassword() != password)
+               if (password.IsEmpty() || !user || !user->ComparePassword(password))
                        user.reset();
        }
 
index d6eec0c15facf73a41bea009dd748e807a364946..46750ffa52022df1ef66d61fda9a8b93a9a08b5b 100644 (file)
@@ -42,6 +42,7 @@ set(base_test_SOURCES
   icinga-notification.cpp
   icinga-perfdata.cpp
   remote-url.cpp
+  remote-user.cpp
   ${base_OBJS}
   $<TARGET_OBJECTS:config>
   $<TARGET_OBJECTS:remote>
@@ -139,6 +140,7 @@ add_boost_test(base
     remote_url/get_and_set
     remote_url/format
     remote_url/illegal_legal_strings
+    api_user/password
 )
 
 if(ICINGA2_WITH_LIVESTATUS)
diff --git a/test/remote-user.cpp b/test/remote-user.cpp
new file mode 100644 (file)
index 0000000..eafa0e7
--- /dev/null
@@ -0,0 +1,52 @@
+/******************************************************************************
+ * Icinga 2                                                                   *
+ * Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/)  *
+ *                                                                            *
+ * This program is free software; you can redistribute it and/or              *
+ * modify it under the terms of the GNU General Public License                *
+ * as published by the Free Software Foundation; either version 2             *
+ * of the License, or (at your option) any later version.                     *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program; if not, write to the Free Software Foundation     *
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.             *
+ ******************************************************************************/
+
+#include "remote/apiuser.hpp"
+#include "base/tlsutility.hpp"
+#include <BoostTestTargetConfig.h>
+
+#include <iostream>
+
+using namespace icinga;
+
+BOOST_AUTO_TEST_SUITE(api_user)
+
+BOOST_AUTO_TEST_CASE(password)
+{
+#ifndef I2_DEBUG
+       std::cout << "Only enabled in Debug builds..." << std::endl;
+#else
+       ApiUser::Ptr user = new ApiUser();
+       String passwd = RandomString(16);
+       String salt = RandomString(8);
+       user->SetPassword("ThisShouldBeIgnored");
+       user->SetPasswordHash(ApiUser::CreateHashedPasswordString(passwd, salt, true));
+
+       BOOST_CHECK(user->GetPasswordHash() != passwd);
+
+       Dictionary::Ptr passwdd = user->GetPasswordDict();
+
+       BOOST_CHECK(passwdd);
+       BOOST_CHECK(passwdd->Get("salt") == salt);
+       BOOST_CHECK(user->ComparePassword(passwd));
+       BOOST_CHECK(!user->ComparePassword("wrong password uwu!"));
+#endif
+}
+
+BOOST_AUTO_TEST_SUITE_END()