From 510e2d622a3f550ab50e19e7761a71d60696862e Mon Sep 17 00:00:00 2001 From: Gunnar Beutner Date: Tue, 22 Aug 2017 14:04:36 +0200 Subject: [PATCH] Implement support for ticket-less certificate requests refs #5450 --- lib/base/tlsutility.cpp | 15 +++ lib/base/tlsutility.hpp | 1 + lib/cli/pkirequestcommand.cpp | 11 +-- lib/cli/pkiutility.cpp | 34 ++++++- lib/cli/pkiutility.hpp | 2 +- lib/remote/CMakeLists.txt | 2 +- lib/remote/jsonrpcconnection-pki.cpp | 133 +++++++++++++++++++++++++++ lib/remote/jsonrpcconnection.cpp | 42 --------- 8 files changed, 189 insertions(+), 51 deletions(-) create mode 100644 lib/remote/jsonrpcconnection-pki.cpp diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index f651d63fe..408178032 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -590,6 +590,21 @@ String CertificateToString(const boost::shared_ptr& cert) return result; } +boost::shared_ptr 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(rawCert, X509_free); +} + String PBKDF2_SHA1(const String& password, const String& salt, int iterations) { unsigned char digest[SHA_DIGEST_LENGTH]; diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp index 59af5c236..2cc824353 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -48,6 +48,7 @@ int I2_BASE_API MakeX509CSR(const String& cn, const String& keyfile, const Strin boost::shared_ptr 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 CertificateToString(const boost::shared_ptr& cert); +boost::shared_ptr I2_BASE_API StringToCertificate(const String& cert); boost::shared_ptr 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 SHA1(const String& s, bool binary = false); diff --git a/lib/cli/pkirequestcommand.cpp b/lib/cli/pkirequestcommand.cpp index a8e84a447..bbbf629a8 100644 --- a/lib/cli/pkirequestcommand.cpp +++ b/lib/cli/pkirequestcommand.cpp @@ -95,17 +95,16 @@ int PKIRequestCommand::Run(const boost::program_options::variables_map& vm, cons return 1; } - if (!vm.count("ticket")) { - Log(LogCritical, "cli", "Ticket (--ticket) must be specified."); - return 1; - } - String port = "5665"; + String ticket; if (vm.count("port")) port = vm["port"].as(); + if (vm.count("ticket")) + ticket = vm["ticket"].as(); + return PkiUtility::RequestCertificate(vm["host"].as(), port, vm["key"].as(), vm["cert"].as(), vm["ca"].as(), GetX509Certificate(vm["trustedcert"].as()), - vm["ticket"].as()); + ticket); } diff --git a/lib/cli/pkiutility.cpp b/lib/cli/pkiutility.cpp index 562089ada..e43a3af66 100644 --- a/lib/cli/pkiutility.cpp +++ b/lib/cli/pkiutility.cpp @@ -259,7 +259,39 @@ int PkiUtility::RequestCertificate(const String& host, const String& port, const Dictionary::Ptr result = response->Get("result"); 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; } diff --git a/lib/cli/pkiutility.hpp b/lib/cli/pkiutility.hpp index a8ae2846b..2a3b1b7c4 100644 --- a/lib/cli/pkiutility.hpp +++ b/lib/cli/pkiutility.hpp @@ -46,7 +46,7 @@ public: static int GenTicket(const String& cn, const String& salt, std::ostream& ticketfp); static int RequestCertificate(const String& host, const String& port, const String& keyfile, const String& certfile, const String& cafile, const boost::shared_ptr& trustedcert, - const String& ticket); + const String& ticket = String()); static String GetCertificateInformation(const boost::shared_ptr& certificate); private: diff --git a/lib/remote/CMakeLists.txt b/lib/remote/CMakeLists.txt index cf27ac463..349091e00 100644 --- a/lib/remote/CMakeLists.txt +++ b/lib/remote/CMakeLists.txt @@ -28,7 +28,7 @@ set(remote_SOURCES configstageshandler.cpp createobjecthandler.cpp deleteobjecthandler.cpp endpoint.cpp endpoint.thpp eventshandler.cpp eventqueue.cpp filterutility.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 typequeryhandler.cpp url.cpp variablequeryhandler.cpp zone.cpp zone.thpp ) diff --git a/lib/remote/jsonrpcconnection-pki.cpp b/lib/remote/jsonrpcconnection-pki.cpp new file mode 100644 index 000000000..14470806e --- /dev/null +++ b/lib/remote/jsonrpcconnection-pki.cpp @@ -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 + +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 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 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 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; +} + diff --git a/lib/remote/jsonrpcconnection.cpp b/lib/remote/jsonrpcconnection.cpp index 5ad7fd0ad..32ffff159 100644 --- a/lib/remote/jsonrpcconnection.cpp +++ b/lib/remote/jsonrpcconnection.cpp @@ -33,8 +33,6 @@ using namespace icinga; static Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params); 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 Timer::Ptr l_JsonRpcConnectionTimeoutTimer; @@ -276,46 +274,6 @@ Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary:: 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 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 newcert = CreateCertIcingaCA(pubkey, subject); - result->Set("cert", CertificateToString(newcert)); - - String cacertfile = GetIcingaCADir() + "/ca.crt"; - boost::shared_ptr cacert = GetX509Certificate(cacertfile); - result->Set("ca", CertificateToString(cacert)); - - return result; -} - void JsonRpcConnection::CheckLiveness(void) { if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) { -- 2.40.0