]> granicus.if.org Git - icinga2/commitdiff
Implement support for ticket-less certificate requests
authorGunnar Beutner <gunnar.beutner@icinga.com>
Tue, 22 Aug 2017 12:04:36 +0000 (14:04 +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/cli/pkirequestcommand.cpp
lib/cli/pkiutility.cpp
lib/cli/pkiutility.hpp
lib/remote/CMakeLists.txt
lib/remote/jsonrpcconnection-pki.cpp [new file with mode: 0644]
lib/remote/jsonrpcconnection.cpp

index f651d63fe0979a90ebcd474231e6730a62c541f5..4081780322d1303b7f7bb7c01382b0556ed5f80d 100644 (file)
@@ -590,6 +590,21 @@ String CertificateToString(const boost::shared_ptr<X509>& cert)
        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)
 {
        unsigned char digest[SHA_DIGEST_LENGTH];
index 59af5c2363af83128ecfc7f795d73d4982286701..2cc824353abd19368456d3bdcd47dbd78da90811 100644 (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);
 String I2_BASE_API GetIcingaCADir(void);
 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);
 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);
index a8e84a447acf39a8462c824fe0da09cc9b3f0b4e..bbbf629a8c9f5f661da261cc50f3e30e39c51a51 100644 (file)
@@ -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<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>(),
            vm["cert"].as<std::string>(), vm["ca"].as<std::string>(), GetX509Certificate(vm["trustedcert"].as<std::string>()),
-           vm["ticket"].as<std::string>());
+           ticket);
 }
index 562089adaaf60d51a0c71d0ccd15f3c4bb9e731d..e43a3af667b71ad9dbddd28955de3c5fc4583c88 100644 (file)
@@ -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;
        }
 
index a8ae2846b946e0e682ceccb61a8bbee6336609f4..2a3b1b7c4b3a4e39caa68701640270702cd76aa4 100644 (file)
@@ -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<X509>& trustedcert,
-           const String& ticket);
+           const String& ticket = String());
        static String GetCertificateInformation(const boost::shared_ptr<X509>& certificate);
 
 private:
index cf27ac463763c3b4b83aaf2ca0fd0b2f1aee0c79..349091e003115c8122cafb75cb4ee47332c77e2b 100644 (file)
@@ -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 (file)
index 0000000..1447080
--- /dev/null
@@ -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;
+}
+
index 5ad7fd0ad7621d6c160bfc07f222d1410da0056d..32ffff159d2170d2d738e85fa953baed246f2fba 100644 (file)
@@ -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<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)
 {
        if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) {