From: Jean Flach Date: Fri, 11 Aug 2017 14:23:24 +0000 (+0200) Subject: Hash API password and comparison X-Git-Tag: v2.8.2~31 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=6504606e23c9ce5f748925dc7445c0053d0f31e8;p=icinga2 Hash API password and comparison fixes #4920 --- diff --git a/doc/09-object-types.md b/doc/09-object-types.md index 2976356f0..33c2763ba 100644 --- a/doc/09-object-types.md +++ b/doc/09-object-types.md @@ -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. diff --git a/doc/11-cli-commands.md b/doc/11-cli-commands.md index c5e9d289a..10acf727a 100644 --- a/doc/11-cli-commands.md +++ b/doc/11-cli-commands.md @@ -19,7 +19,8 @@ Usage: icinga2 [] 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 -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 [] 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 diff --git a/doc/12-icinga2-api.md b/doc/12-icinga2-api.md index b2e61765b..8351c90e0 100644 --- a/doc/12-icinga2-api.md +++ b/doc/12-icinga2-api.md @@ -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 The Icinga 2 API allows you to manage configuration objects diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index eda13ba1d..f8bc136ad 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -624,6 +624,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(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]; diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp index c2191cc51..2ab230f6c 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -52,6 +52,7 @@ boost::shared_ptr I2_BASE_API StringToCertificate(const String& cert); boost::shared_ptr I2_BASE_API CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject); boost::shared_ptr I2_BASE_API CreateCertIcingaCA(const boost::shared_ptr& cert); String I2_BASE_API PBKDF2_SHA1(const String& password, const String& salt, int iterations); +String I2_BASE_API PBKDF2_SHA256(const String& password, const String& salt, int iterations); String I2_BASE_API SHA1(const String& s, bool binary = false); String I2_BASE_API SHA256(const String& s); String I2_BASE_API RandomString(int length); diff --git a/lib/cli/CMakeLists.txt b/lib/cli/CMakeLists.txt index 101577530..f4783fa6f 100644 --- a/lib/cli/CMakeLists.txt +++ b/lib/cli/CMakeLists.txt @@ -26,7 +26,7 @@ set(cli_SOURCES objectlistcommand.cpp objectlistutility.cpp pkinewcacommand.cpp pkinewcertcommand.cpp pkisigncsrcommand.cpp pkirequestcommand.cpp pkisavecertcommand.cpp pkiticketcommand.cpp variablegetcommand.cpp variablelistcommand.cpp variableutility.cpp - troubleshootcommand.cpp + apiusercommand.cpp troubleshootcommand.cpp ) if(ICINGA2_UNITY_BUILD) diff --git a/lib/cli/apisetupcommand.cpp b/lib/cli/apisetupcommand.cpp index 6c6d79edd..9a91642f9 100644 --- a/lib/cli/apisetupcommand.cpp +++ b/lib/cli/apisetupcommand.cpp @@ -36,7 +36,7 @@ String ApiSetupCommand::GetDescription(void) const String ApiSetupCommand::GetShortDescription(void) const { - return "setup for api"; + return "setup for API"; } ImpersonationLevel ApiSetupCommand::GetImpersonationLevel(void) const diff --git a/lib/cli/apiusercommand.cpp b/lib/cli/apiusercommand.cpp new file mode 100644 index 000000000..9d43e120f --- /dev/null +++ b/lib/cli/apiusercommand.cpp @@ -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 + +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(), "API username") + ("passwd", po::value(), "Password in clear text") + ("salt", po::value(), "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& 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(); + String passwd = vm["passwd"].as(); + String salt = vm.count("salt") ? String(vm["salt"].as()) : 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 index 000000000..4a4bfb280 --- /dev/null +++ b/lib/cli/apiusercommand.hpp @@ -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& ap) const override; +}; + +} + +#endif /* APIUSERCOMMAND_H */ diff --git a/lib/remote/apiuser.cpp b/lib/remote/apiuser.cpp index c3e95ebe6..fb847f9bb 100644 --- a/lib/remote/apiuser.cpp +++ b/lib/remote/apiuser.cpp @@ -21,11 +21,20 @@ #include "remote/apiuser.tcpp" #include "base/configtype.hpp" #include "base/base64.hpp" +#include "base/tlsutility.hpp" using namespace icinga; REGISTER_TYPE(ApiUser); +void ApiUser::OnConfigLoaded(void) +{ + ObjectImpl::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()) { @@ -63,3 +72,50 @@ ApiUser::Ptr ApiUser::GetByAuthHeader(const String& auth_header) return user; } + +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); + +} diff --git a/lib/remote/apiuser.hpp b/lib/remote/apiuser.hpp index 4021487b4..09ee1a4d0 100644 --- a/lib/remote/apiuser.hpp +++ b/lib/remote/apiuser.hpp @@ -35,8 +35,14 @@ public: DECLARE_OBJECT(ApiUser); DECLARE_OBJECTNAME(ApiUser); + virtual void OnConfigLoaded(void) override; + static ApiUser::Ptr GetByClientCN(const String& cn); static ApiUser::Ptr GetByAuthHeader(const String& auth_header); + static String CreateHashedPasswordString(const String& password, const String& salt, const bool shadow = false); + + Dictionary::Ptr GetPasswordDict(void) const; + bool ComparePassword(String password) const; }; } diff --git a/lib/remote/apiuser.ti b/lib/remote/apiuser.ti index effc804c2..32557d169 100644 --- a/lib/remote/apiuser.ti +++ b/lib/remote/apiuser.ti @@ -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; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2a234419d..9d993feae 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,6 +27,7 @@ set(base_test_SOURCES base-value.cpp config-ops.cpp icinga-checkresult.cpp icinga-macros.cpp icinga-notification.cpp icinga-perfdata.cpp remote-url.cpp + remote-user.cpp ) if(ICINGA2_UNITY_BUILD) @@ -118,6 +119,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 index 000000000..eafa0e721 --- /dev/null +++ b/test/remote-user.cpp @@ -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 + +#include + +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()