Implement support for ticket-less certificate requests

refs #5450
This commit is contained in:
Gunnar Beutner 2017-08-22 14:04:36 +02:00
parent 1d75a15d8e
commit 510e2d622a
8 changed files with 189 additions and 51 deletions

View File

@ -590,6 +590,21 @@ String CertificateToString(const boost::shared_ptr<X509>& cert)
return result; return result;
} }
boost::shared_ptr<X509> StringToCertificate(const String& cert)
{
BIO *bio = BIO_new(BIO_s_mem());
BIO_write(bio, (const void *)cert.CStr(), cert.GetLength());
X509 *rawCert = PEM_read_bio_X509_AUX(bio, NULL, NULL, NULL);
BIO_free(bio);
if (!rawCert)
BOOST_THROW_EXCEPTION(std::invalid_argument("The specified X509 certificate is invalid."));
return boost::shared_ptr<X509>(rawCert, X509_free);
}
String PBKDF2_SHA1(const String& password, const String& salt, int iterations) String PBKDF2_SHA1(const String& password, const String& salt, int iterations)
{ {
unsigned char digest[SHA_DIGEST_LENGTH]; unsigned char digest[SHA_DIGEST_LENGTH];

View File

@ -48,6 +48,7 @@ int I2_BASE_API MakeX509CSR(const String& cn, const String& keyfile, const Strin
boost::shared_ptr<X509> I2_BASE_API CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca); boost::shared_ptr<X509> I2_BASE_API CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca);
String I2_BASE_API GetIcingaCADir(void); String I2_BASE_API GetIcingaCADir(void);
String I2_BASE_API CertificateToString(const boost::shared_ptr<X509>& cert); String I2_BASE_API CertificateToString(const boost::shared_ptr<X509>& cert);
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(EVP_PKEY *pubkey, X509_NAME *subject);
String I2_BASE_API PBKDF2_SHA1(const String& password, const String& salt, int iterations); String I2_BASE_API PBKDF2_SHA1(const String& password, const String& salt, int iterations);
String I2_BASE_API SHA1(const String& s, bool binary = false); String I2_BASE_API SHA1(const String& s, bool binary = false);

View File

@ -95,17 +95,16 @@ int PKIRequestCommand::Run(const boost::program_options::variables_map& vm, cons
return 1; return 1;
} }
if (!vm.count("ticket")) {
Log(LogCritical, "cli", "Ticket (--ticket) must be specified.");
return 1;
}
String port = "5665"; String port = "5665";
String ticket;
if (vm.count("port")) if (vm.count("port"))
port = vm["port"].as<std::string>(); port = vm["port"].as<std::string>();
if (vm.count("ticket"))
ticket = vm["ticket"].as<std::string>();
return PkiUtility::RequestCertificate(vm["host"].as<std::string>(), port, vm["key"].as<std::string>(), return PkiUtility::RequestCertificate(vm["host"].as<std::string>(), port, vm["key"].as<std::string>(),
vm["cert"].as<std::string>(), vm["ca"].as<std::string>(), GetX509Certificate(vm["trustedcert"].as<std::string>()), vm["cert"].as<std::string>(), vm["ca"].as<std::string>(), GetX509Certificate(vm["trustedcert"].as<std::string>()),
vm["ticket"].as<std::string>()); ticket);
} }

View File

@ -259,7 +259,39 @@ int PkiUtility::RequestCertificate(const String& host, const String& port, const
Dictionary::Ptr result = response->Get("result"); Dictionary::Ptr result = response->Get("result");
if (result->Contains("error")) { if (result->Contains("error")) {
Log(LogCritical, "cli", result->Get("error")); LogSeverity severity;
if (result->Get("status_code") == 1)
severity = LogCritical;
else {
severity = LogInformation;
Log(severity, "cli", "!!!!!!");
}
Log(severity, "cli")
<< "!!! " << result->Get("error");
if (result->Get("status_code") == 1)
return 1;
else {
Log(severity, "cli", "!!!!!!");
return 0;
}
}
try {
StringToCertificate(result->Get("cert"));
} catch (const std::exception& ex) {
Log(LogCritical, "cli")
<< "Could not write certificate file: " << DiagnosticInformation(ex, false);
return 1;
}
try {
StringToCertificate(result->Get("ca"));
} catch (const std::exception& ex) {
Log(LogCritical, "cli")
<< "Could not write CA file: " << DiagnosticInformation(ex, false);
return 1; return 1;
} }

View File

@ -46,7 +46,7 @@ public:
static int GenTicket(const String& cn, const String& salt, std::ostream& ticketfp); static int GenTicket(const String& cn, const String& salt, std::ostream& ticketfp);
static int RequestCertificate(const String& host, const String& port, const String& keyfile, static int RequestCertificate(const String& host, const String& port, const String& keyfile,
const String& certfile, const String& cafile, const boost::shared_ptr<X509>& trustedcert, const String& certfile, const String& cafile, const boost::shared_ptr<X509>& trustedcert,
const String& ticket); const String& ticket = String());
static String GetCertificateInformation(const boost::shared_ptr<X509>& certificate); static String GetCertificateInformation(const boost::shared_ptr<X509>& certificate);
private: private:

View File

@ -28,7 +28,7 @@ set(remote_SOURCES
configstageshandler.cpp createobjecthandler.cpp deleteobjecthandler.cpp configstageshandler.cpp createobjecthandler.cpp deleteobjecthandler.cpp
endpoint.cpp endpoint.thpp eventshandler.cpp eventqueue.cpp filterutility.cpp endpoint.cpp endpoint.thpp eventshandler.cpp eventqueue.cpp filterutility.cpp
httpchunkedencoding.cpp httpclientconnection.cpp httpserverconnection.cpp httphandler.cpp httprequest.cpp httpresponse.cpp httpchunkedencoding.cpp httpclientconnection.cpp httpserverconnection.cpp httphandler.cpp httprequest.cpp httpresponse.cpp
httputility.cpp infohandler.cpp jsonrpc.cpp jsonrpcconnection.cpp jsonrpcconnection-heartbeat.cpp httputility.cpp infohandler.cpp jsonrpc.cpp jsonrpcconnection.cpp jsonrpcconnection-heartbeat.cpp jsonrpcconnection-pki.cpp
messageorigin.cpp modifyobjecthandler.cpp statushandler.cpp objectqueryhandler.cpp templatequeryhandler.cpp messageorigin.cpp modifyobjecthandler.cpp statushandler.cpp objectqueryhandler.cpp templatequeryhandler.cpp
typequeryhandler.cpp url.cpp variablequeryhandler.cpp zone.cpp zone.thpp typequeryhandler.cpp url.cpp variablequeryhandler.cpp zone.cpp zone.thpp
) )

View File

@ -0,0 +1,133 @@
/******************************************************************************
* 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/jsonrpcconnection.hpp"
#include "remote/apilistener.hpp"
#include "remote/apifunction.hpp"
#include "base/configtype.hpp"
#include "base/objectlock.hpp"
#include "base/utility.hpp"
#include "base/logger.hpp"
#include "base/exception.hpp"
#include "base/convert.hpp"
#include <boost/thread/once.hpp>
using namespace icinga;
static Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler);
Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
if (!params)
return Empty;
String certText = params->Get("cert_request");
boost::shared_ptr<X509> cert;
Dictionary::Ptr result = new Dictionary();
if (certText.IsEmpty())
cert = origin->FromClient->GetStream()->GetPeerCertificate();
else
cert = StringToCertificate(certText);
unsigned int n;
unsigned char digest[EVP_MAX_MD_SIZE];
if (!X509_digest(cert.get(), EVP_sha256(), digest, &n)) {
result->Set("status_code", 1);
result->Set("error", "Could not calculate fingerprint for the X509 certificate.");
return result;
}
char certFingerprint[EVP_MAX_MD_SIZE*2+1];
for (unsigned int i = 0; i < n; i++)
sprintf(certFingerprint + 2 * i, "%02x", digest[i]);
String requestDir = Application::GetLocalStateDir() + "/lib/icinga2/pki-requests";
String requestPath = requestDir + "/" + certFingerprint + ".json";
ApiListener::Ptr listener = ApiListener::GetInstance();
String cacertfile = listener->GetCaPath();
boost::shared_ptr<X509> cacert = GetX509Certificate(cacertfile);
result->Set("ca", CertificateToString(cacert));
if (Utility::PathExists(requestPath)) {
Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
String certResponse = request->Get("cert_response");
if (!certResponse.IsEmpty()) {
result->Set("cert", certResponse);
result->Set("status_code", 0);
return result;
}
}
boost::shared_ptr<X509> newcert;
EVP_PKEY *pubkey;
X509_NAME *subject;
if (!Utility::PathExists(GetIcingaCADir() + "/ca.key"))
goto delayed_request;
if (!origin->FromClient->IsAuthenticated()) {
String salt = listener->GetTicketSalt();
if (salt.IsEmpty())
goto delayed_request;
String ticket = params->Get("ticket");
String realTicket = PBKDF2_SHA1(origin->FromClient->GetIdentity(), salt, 50000);
if (ticket != realTicket) {
result->Set("status_code", 1);
result->Set("error", "Invalid ticket.");
return result;
}
}
pubkey = X509_get_pubkey(cert.get());
subject = X509_get_subject_name(cert.get());
newcert = CreateCertIcingaCA(pubkey, subject);
result->Set("cert", CertificateToString(newcert));
result->Set("status_code", 0);
return result;
delayed_request:
Utility::MkDirP(requestDir, 0700);
Dictionary::Ptr request = new Dictionary();
request->Set("cert_request", CertificateToString(cert));
request->Set("ticket", params->Get("ticket"));
Utility::SaveJsonFile(requestPath, 0600, request);
result->Set("status_code", 2);
result->Set("error", "Certificate request is pending. Waiting for approval from the parent Icinga instance.");
return result;
}

View File

@ -33,8 +33,6 @@ using namespace icinga;
static Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params); static Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
REGISTER_APIFUNCTION(SetLogPosition, log, &SetLogPositionHandler); REGISTER_APIFUNCTION(SetLogPosition, log, &SetLogPositionHandler);
static Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler);
static boost::once_flag l_JsonRpcConnectionOnceFlag = BOOST_ONCE_INIT; static boost::once_flag l_JsonRpcConnectionOnceFlag = BOOST_ONCE_INIT;
static Timer::Ptr l_JsonRpcConnectionTimeoutTimer; static Timer::Ptr l_JsonRpcConnectionTimeoutTimer;
@ -276,46 +274,6 @@ Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::
return Empty; return Empty;
} }
Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
if (!params)
return Empty;
Dictionary::Ptr result = new Dictionary();
if (!origin->FromClient->IsAuthenticated()) {
ApiListener::Ptr listener = ApiListener::GetInstance();
String salt = listener->GetTicketSalt();
if (salt.IsEmpty()) {
result->Set("error", "Ticket salt is not configured.");
return result;
}
String ticket = params->Get("ticket");
String realTicket = PBKDF2_SHA1(origin->FromClient->GetIdentity(), salt, 50000);
if (ticket != realTicket) {
result->Set("error", "Invalid ticket.");
return result;
}
}
boost::shared_ptr<X509> cert = origin->FromClient->GetStream()->GetPeerCertificate();
EVP_PKEY *pubkey = X509_get_pubkey(cert.get());
X509_NAME *subject = X509_get_subject_name(cert.get());
boost::shared_ptr<X509> newcert = CreateCertIcingaCA(pubkey, subject);
result->Set("cert", CertificateToString(newcert));
String cacertfile = GetIcingaCADir() + "/ca.crt";
boost::shared_ptr<X509> cacert = GetX509Certificate(cacertfile);
result->Set("ca", CertificateToString(cacert));
return result;
}
void JsonRpcConnection::CheckLiveness(void) void JsonRpcConnection::CheckLiveness(void)
{ {
if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) { if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) {