From 0ec07bce518d1b5ccc0271b6d13992f2a67fb16b Mon Sep 17 00:00:00 2001 From: Gunnar Beutner Date: Tue, 29 Aug 2017 14:37:13 +0200 Subject: [PATCH] Implement support for updating client certificates refs #5450 --- lib/base/tlsutility.cpp | 20 ++++++ lib/base/tlsutility.hpp | 1 + lib/remote/apilistener.cpp | 16 +---- lib/remote/apilistener.hpp | 1 - lib/remote/jsonrpcconnection-pki.cpp | 96 +++++++++++++++++++++++++++- lib/remote/jsonrpcconnection.cpp | 33 +++++++++- lib/remote/jsonrpcconnection.hpp | 13 ++++ 7 files changed, 161 insertions(+), 19 deletions(-) diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index d71dc5124..8876e7bb5 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -727,4 +727,24 @@ String RandomString(int length) return result; } +bool VerifyCertificate(const boost::shared_ptr& caCertificate, const boost::shared_ptr& certificate) +{ + X509_STORE *store = X509_STORE_new(); + + if (!store) + return false; + + X509_STORE_add_cert(store, caCertificate.get()); + + X509_STORE_CTX *csc = X509_STORE_CTX_new(); + X509_STORE_CTX_init(csc, store, certificate.get(), NULL); + + int rc = X509_verify_cert(csc); + + X509_STORE_CTX_free(csc); + X509_STORE_free(store); + + return rc == 1; +} + } diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp index 0657c94a5..c2191cc51 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -55,6 +55,7 @@ String I2_BASE_API PBKDF2_SHA1(const String& password, const String& salt, int i 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); +bool I2_BASE_API VerifyCertificate(const boost::shared_ptr& caCertificate, const boost::shared_ptr& certificate); class I2_BASE_API openssl_error : virtual public std::exception, virtual public boost::exception { }; diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp index bd6630dce..fe8dab901 100644 --- a/lib/remote/apilistener.cpp +++ b/lib/remote/apilistener.cpp @@ -395,7 +395,6 @@ void ApiListener::NewClientHandlerInternal(const Socket::Ptr& client, const Stri Log(LogWarning, "ApiListener") << "Certificate validation failed for endpoint '" << hostname << "': " << tlsStream->GetVerifyError(); - return; } } @@ -484,7 +483,7 @@ void ApiListener::SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoi Log(LogInformation, "ApiListener") << "Requesting new certificate for this Icinga instance from endpoint '" << endpoint->GetName() << "'."; - SendCertificateRequest(aclient); + aclient->SendCertificateRequest(); } /* Make sure that the config updates are synced @@ -539,19 +538,6 @@ void ApiListener::SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoi << "Finished syncing endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'."; } -void ApiListener::SendCertificateRequest(const JsonRpcConnection::Ptr& aclient) -{ - Dictionary::Ptr message = new Dictionary(); - message->Set("jsonrpc", "2.0"); - message->Set("method", "pki::RequestCertificate"); - - Dictionary::Ptr params = new Dictionary(); - - message->Set("params", params); - - JsonRpc::SendMessage(aclient->GetStream(), message); -} - void ApiListener::ApiTimerHandler(void) { double now = Utility::GetTime(); diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp index c16ed46c5..8e12f0dca 100644 --- a/lib/remote/apilistener.hpp +++ b/lib/remote/apilistener.hpp @@ -158,7 +158,6 @@ private: static void ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file); void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient); - void SendCertificateRequest(const JsonRpcConnection::Ptr& aclient); /* configsync */ void UpdateConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin, diff --git a/lib/remote/jsonrpcconnection-pki.cpp b/lib/remote/jsonrpcconnection-pki.cpp index a0df4fa0b..fe7c666dd 100644 --- a/lib/remote/jsonrpcconnection-pki.cpp +++ b/lib/remote/jsonrpcconnection-pki.cpp @@ -20,6 +20,7 @@ #include "remote/jsonrpcconnection.hpp" #include "remote/apilistener.hpp" #include "remote/apifunction.hpp" +#include "remote/jsonrpc.hpp" #include "base/configtype.hpp" #include "base/objectlock.hpp" #include "base/utility.hpp" @@ -27,6 +28,7 @@ #include "base/exception.hpp" #include "base/convert.hpp" #include +#include using namespace icinga; @@ -67,8 +69,7 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona ApiListener::Ptr listener = ApiListener::GetInstance(); - String cacertfile = listener->GetCaPath(); - boost::shared_ptr cacert = GetX509Certificate(cacertfile); + boost::shared_ptr cacert = GetX509Certificate(listener->GetCaPath()); result->Set("ca", CertificateToString(cacert)); if (Utility::PathExists(requestPath)) { @@ -112,6 +113,18 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona subject = X509_get_subject_name(cert.get()); newcert = CreateCertIcingaCA(pubkey, subject); + + /* verify that the new cert matches the CA we're using for the ApiListener; + * this ensures that the CA we have in /var/lib/icinga2/ca matches the one + * we're using for cluster connections (there's no point in sending a client + * a certificate it wouldn't be able to use to connect to us anyway) */ + if (!VerifyCertificate(cacert, newcert)) { + Log(LogWarning, "JsonRpcConnection") + << "The CA in '" << listener->GetCaPath() << "' does not match the CA which Icinga uses " + << "for its own cluster connections. This is most likely a configuration problem."; + goto delayed_request; + } + result->Set("cert", CertificateToString(newcert)); result->Set("status_code", 0); @@ -132,3 +145,82 @@ delayed_request: return result; } +void JsonRpcConnection::SendCertificateRequest(void) +{ + Dictionary::Ptr message = new Dictionary(); + message->Set("jsonrpc", "2.0"); + message->Set("method", "pki::RequestCertificate"); + + String id = Utility::NewUniqueID(); + message->Set("id", id); + + Dictionary::Ptr params = new Dictionary(); + + ApiListener::Ptr listener = ApiListener::GetInstance(); + + if (listener) + params->Set("ticket", listener->GetClientTicket()); + + message->Set("params", params); + + RegisterCallback(id, boost::bind(&JsonRpcConnection::CertificateRequestResponseHandler, this, _1)); + + JsonRpc::SendMessage(GetStream(), message); +} + +void JsonRpcConnection::CertificateRequestResponseHandler(const Dictionary::Ptr& message) +{ + Log(LogWarning, "JsonRpcConnection") + << message->ToString(); + + Dictionary::Ptr result = message->Get("result"); + + if (!result) + return; + + String ca = result->Get("ca"); + String cert = result->Get("cert"); + int status = result->Get("status_code"); + + /* TODO: make sure the cert's public key matches ours */ + + if (status != 0) { + /* TODO: log error */ + return; + } + + ApiListener::Ptr listener = ApiListener::GetInstance(); + + if (!listener) + return; + + String caPath = listener->GetCaPath(); + + std::fstream cafp; + String tempCaPath = Utility::CreateTempFile(caPath + ".XXXXXX", 0644, cafp); + cafp << ca; + cafp.close(); + + if (rename(tempCaPath.CStr(), caPath.CStr()) < 0) { + BOOST_THROW_EXCEPTION(posix_error() + << boost::errinfo_api_function("rename") + << boost::errinfo_errno(errno) + << boost::errinfo_file_name(tempCaPath)); + } + + String certPath = listener->GetCertPath(); + + std::fstream certfp; + String tempCertPath = Utility::CreateTempFile(certPath + ".XXXXXX", 0644, certfp); + certfp << cert; + certfp.close(); + + if (rename(tempCertPath.CStr(), certPath.CStr()) < 0) { + BOOST_THROW_EXCEPTION(posix_error() + << boost::errinfo_api_function("rename") + << boost::errinfo_errno(errno) + << boost::errinfo_file_name(tempCertPath)); + } + + /* Update ApiListener's SSL_CTX */ +} diff --git a/lib/remote/jsonrpcconnection.cpp b/lib/remote/jsonrpcconnection.cpp index 32ffff159..92c0e1c48 100644 --- a/lib/remote/jsonrpcconnection.cpp +++ b/lib/remote/jsonrpcconnection.cpp @@ -184,7 +184,30 @@ void JsonRpcConnection::MessageHandler(const String& jsonString) origin->FromZone = Zone::GetByName(message->Get("originZone")); } - String method = message->Get("method"); + Value vmethod; + + if (!message->Get("method", &vmethod)) { + Value vid; + + if (!message->Get("id", &vid)) + return; + + String id = vid; + + auto it = m_ApiCallbacks.find(id); + + if (it == m_ApiCallbacks.end()) + return; + + ApiCallbackInfo aci = it->second; + m_ApiCallbacks.erase(it); + + aci.Callback(message); + + return; + } + + String method = vmethod; Log(LogNotice, "JsonRpcConnection") << "Received '" << method << "' message from '" << m_Identity << "'"; @@ -330,3 +353,11 @@ double JsonRpcConnection::GetWorkQueueRate(void) return rate / count; } +void JsonRpcConnection::RegisterCallback(const String& id, const boost::function& callback) +{ + ApiCallbackInfo aci; + aci.Timestamp = Utility::GetTime(); + aci.Callback = callback; + + m_ApiCallbacks[id] = aci; +} diff --git a/lib/remote/jsonrpcconnection.hpp b/lib/remote/jsonrpcconnection.hpp index 62c134c27..d91125485 100644 --- a/lib/remote/jsonrpcconnection.hpp +++ b/lib/remote/jsonrpcconnection.hpp @@ -43,6 +43,12 @@ enum ClientType class MessageOrigin; +struct ApiCallbackInfo +{ + double Timestamp; + boost::function Callback; +}; + /** * An API client connection. * @@ -75,6 +81,8 @@ public: static int GetWorkQueueLength(void); static double GetWorkQueueRate(void); + void SendCertificateRequest(void); + private: int m_ID; String m_Identity; @@ -87,6 +95,7 @@ private: double m_NextHeartbeat; double m_HeartbeatTimeout; boost::mutex m_DataHandlerMutex; + std::map m_ApiCallbacks; StreamReadContext m_Context; @@ -98,6 +107,10 @@ private: static void StaticInitialize(void); static void TimeoutTimerHandler(void); void CheckLiveness(void); + + void RegisterCallback(const String& id, const boost::function& callback); + + void CertificateRequestResponseHandler(const Dictionary::Ptr& message); }; } -- 2.40.0