]> granicus.if.org Git - icinga2/commitdiff
Implement support for updating client certificates
authorGunnar Beutner <gunnar.beutner@icinga.com>
Tue, 29 Aug 2017 12:37:13 +0000 (14:37 +0200)
committerGunnar Beutner <gunnar.beutner@icinga.com>
Tue, 12 Sep 2017 10:52:49 +0000 (12:52 +0200)
refs #5450

lib/base/tlsutility.cpp
lib/base/tlsutility.hpp
lib/remote/apilistener.cpp
lib/remote/apilistener.hpp
lib/remote/jsonrpcconnection-pki.cpp
lib/remote/jsonrpcconnection.cpp
lib/remote/jsonrpcconnection.hpp

index d71dc5124311e71a2c97b6de3720f61cf0bd11f5..8876e7bb5ef520a273c50b9b639c578fd1ac5a03 100644 (file)
@@ -727,4 +727,24 @@ String RandomString(int length)
        return result;
 }
 
+bool VerifyCertificate(const boost::shared_ptr<X509>& caCertificate, const boost::shared_ptr<X509>& 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;
+}
+
 }
index 0657c94a5dd50cb87f693247b85f08983771f575..c2191cc51cda726bf9ea620225ace80ca7cc60ca 100644 (file)
@@ -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<X509>& caCertificate, const boost::shared_ptr<X509>& certificate);
 
 class I2_BASE_API openssl_error : virtual public std::exception, virtual public boost::exception { };
 
index bd6630dce4aeca9a98b4509311eef03ecfcb48e9..fe8dab901bd7482543fb27daf7d3ab41d7c79911 100644 (file)
@@ -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();
index c16ed46c53bbe77c8b672ed9c0e72f7d8a9b5de9..8e12f0dcae147c40eb8e4e8d9ec0b22c272f70fe 100644 (file)
@@ -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,
index a0df4fa0b86210b4155e66b0658f0b86c8475749..fe7c666dde744add46e260c6210bd9b45c76fa22 100644 (file)
@@ -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 <boost/thread/once.hpp>
+#include <fstream>
 
 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<X509> cacert = GetX509Certificate(cacertfile);
+       boost::shared_ptr<X509> 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 */
+}
index 32ffff159d2170d2d738e85fa953baed246f2fba..92c0e1c480fa16d4c5e21e3d675705ab321c054a 100644 (file)
@@ -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<void (const Dictionary::Ptr&)>& callback)
+{
+       ApiCallbackInfo aci;
+       aci.Timestamp = Utility::GetTime();
+       aci.Callback = callback;
+
+       m_ApiCallbacks[id] = aci;
+}
index 62c134c277ae4f2c6c72d12954354e035a24a1df..d91125485f0eef87ede753e4e76be5ad17db0bb8 100644 (file)
@@ -43,6 +43,12 @@ enum ClientType
 
 class MessageOrigin;
 
+struct ApiCallbackInfo
+{
+       double Timestamp;
+       boost::function<void (const Dictionary::Ptr&)> 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<String, ApiCallbackInfo> 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<void (const Dictionary::Ptr&)>& callback);
+
+       void CertificateRequestResponseHandler(const Dictionary::Ptr& message);
 };
 
 }