From: Jean Flach Date: Fri, 11 Aug 2017 14:23:24 +0000 (+0200) Subject: Hash API password and comparison X-Git-Tag: v2.9.0~155^2~9 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=92e2faaa08d1bc2872fba0c5f2859a24c7e159bd;p=icinga2 Hash API password and comparison fixes #4920 --- diff --git a/doc/09-object-types.md b/doc/09-object-types.md index b0894b62b..b846964db 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 58aaf6873..b5a883c90 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 3955ceca4..1f74a2735 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -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(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 7928c56d1..38df7e587 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -51,6 +51,7 @@ std::shared_ptr StringToCertificate(const String& cert); std::shared_ptr CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject); std::shared_ptr CreateCertIcingaCA(const std::shared_ptr& 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); diff --git a/lib/cli/CMakeLists.txt b/lib/cli/CMakeLists.txt index bb2c75f47..7a147f82f 100644 --- a/lib/cli/CMakeLists.txt +++ b/lib/cli/CMakeLists.txt @@ -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 diff --git a/lib/cli/apisetupcommand.cpp b/lib/cli/apisetupcommand.cpp index 1345dc92b..bc832c525 100644 --- a/lib/cli/apisetupcommand.cpp +++ b/lib/cli/apisetupcommand.cpp @@ -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 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 183291af3..ea3eb7849 100644 --- a/lib/remote/apiuser.cpp +++ b/lib/remote/apiuser.cpp @@ -20,11 +20,20 @@ #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::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()) { @@ -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); + +} diff --git a/lib/remote/apiuser.hpp b/lib/remote/apiuser.hpp index 755273bf4..eeae3c77b 100644 --- a/lib/remote/apiuser.hpp +++ b/lib/remote/apiuser.hpp @@ -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; }; } diff --git a/lib/remote/apiuser.ti b/lib/remote/apiuser.ti index aef71fd4f..a767f833a 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/lib/remote/httpserverconnection.cpp b/lib/remote/httpserverconnection.cpp index 1b19635a4..4fbd774b7 100644 --- a/lib/remote/httpserverconnection.cpp +++ b/lib/remote/httpserverconnection.cpp @@ -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(); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d6eec0c15..46750ffa5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -42,6 +42,7 @@ set(base_test_SOURCES icinga-notification.cpp icinga-perfdata.cpp remote-url.cpp + remote-user.cpp ${base_OBJS} $ $ @@ -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 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()