Hash API password and comparison

fixes #4920
This commit is contained in:
Jean Flach 2017-08-11 16:23:24 +02:00
parent adc054097d
commit 6504606e23
14 changed files with 292 additions and 8 deletions

View 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.

View 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

View 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

View File

@ -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<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];

View File

@ -52,6 +52,7 @@ boost::shared_ptr<X509> I2_BASE_API StringToCertificate(const String& cert);
boost::shared_ptr<X509> I2_BASE_API CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject);
boost::shared_ptr<X509> I2_BASE_API CreateCertIcingaCA(const boost::shared_ptr<X509>& 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);

View File

@ -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)

View File

@ -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

View File

@ -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;
}

View File

@ -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 */

View File

@ -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<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>()) {
@ -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);
}

View File

@ -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;
};
}

View 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;
};

View File

@ -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)

52
test/remote-user.cpp Normal file
View File

@ -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()