Hash API password and comparison

fixes #4920
This commit is contained in:
Jean Flach 2017-08-11 16:23:24 +02:00 committed by Gunnar Beutner
parent 34a046cd00
commit 92e2faaa08
15 changed files with 293 additions and 10 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

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

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

View 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

View 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

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

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

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

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

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

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

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