summaryrefslogtreecommitdiffstats
path: root/lib/remote/jsonrpcconnection-pki.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/remote/jsonrpcconnection-pki.cpp')
-rw-r--r--lib/remote/jsonrpcconnection-pki.cpp439
1 files changed, 439 insertions, 0 deletions
diff --git a/lib/remote/jsonrpcconnection-pki.cpp b/lib/remote/jsonrpcconnection-pki.cpp
new file mode 100644
index 0000000..340e12b
--- /dev/null
+++ b/lib/remote/jsonrpcconnection-pki.cpp
@@ -0,0 +1,439 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/jsonrpc.hpp"
+#include "base/atomic-file.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>
+#include <boost/regex.hpp>
+#include <fstream>
+#include <openssl/asn1.h>
+#include <openssl/ssl.h>
+#include <openssl/x509.h>
+
+using namespace icinga;
+
+static Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler);
+static Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+REGISTER_APIFUNCTION(UpdateCertificate, pki, &UpdateCertificateHandler);
+
+Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ String certText = params->Get("cert_request");
+
+ std::shared_ptr<X509> cert;
+
+ Dictionary::Ptr result = new Dictionary();
+ auto& tlsConn (origin->FromClient->GetStream()->next_layer());
+
+ /* Use the presented client certificate if not provided. */
+ if (certText.IsEmpty()) {
+ cert = tlsConn.GetPeerCertificate();
+ } else {
+ cert = StringToCertificate(certText);
+ }
+
+ if (!cert) {
+ Log(LogWarning, "JsonRpcConnection") << "No certificate or CSR received";
+
+ result->Set("status_code", 1);
+ result->Set("error", "No certificate or CSR received.");
+
+ return result;
+ }
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+ std::shared_ptr<X509> cacert = GetX509Certificate(listener->GetDefaultCaPath());
+
+ String cn = GetCertificateCN(cert);
+
+ bool signedByCA = false;
+
+ {
+ Log logmsg(LogInformation, "JsonRpcConnection");
+ logmsg << "Received certificate request for CN '" << cn << "'";
+
+ try {
+ signedByCA = VerifyCertificate(cacert, cert, listener->GetCrlPath());
+ if (!signedByCA) {
+ logmsg << " not";
+ }
+ logmsg << " signed by our CA.";
+ } catch (const std::exception &ex) {
+ logmsg << " which couldn't be verified";
+
+ if (const unsigned long *openssl_code = boost::get_error_info<errinfo_openssl_error>(ex)) {
+ logmsg << ": " << X509_verify_cert_error_string(long(*openssl_code)) << " (code " << *openssl_code << ")";
+ } else {
+ logmsg << ".";
+ }
+ }
+ }
+
+ std::shared_ptr<X509> parsedRequestorCA;
+ X509* requestorCA = nullptr;
+
+ if (signedByCA) {
+ bool uptodate = IsCertUptodate(cert);
+
+ if (uptodate) {
+ // Even if the leaf is up-to-date, the root may expire soon.
+ // In a regular setup where Icinga manages the PKI, there is only one CA.
+ // Icinga includes it in handshakes, let's see whether the peer needs a fresh one...
+
+ if (cn == origin->FromClient->GetIdentity()) {
+ auto chain (SSL_get_peer_cert_chain(tlsConn.native_handle()));
+
+ if (chain) {
+ auto len (sk_X509_num(chain));
+
+ for (int i = 0; i < len; ++i) {
+ auto link (sk_X509_value(chain, i));
+
+ if (!X509_NAME_cmp(X509_get_subject_name(link), X509_get_issuer_name(link))) {
+ requestorCA = link;
+ }
+ }
+ }
+ } else {
+ Value requestorCaStr;
+
+ if (params->Get("requestor_ca", &requestorCaStr)) {
+ parsedRequestorCA = StringToCertificate(requestorCaStr);
+ requestorCA = parsedRequestorCA.get();
+ }
+ }
+
+ if (requestorCA && !IsCaUptodate(requestorCA)) {
+ int days;
+
+ if (ASN1_TIME_diff(&days, nullptr, X509_get_notAfter(requestorCA), X509_get_notAfter(cacert.get())) && days > 0) {
+ uptodate = false;
+ }
+ }
+ }
+
+ if (uptodate) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "The certificates for CN '" << cn << "' and its root CA are valid and uptodate. Skipping automated renewal.";
+ result->Set("status_code", 1);
+ result->Set("error", "The certificates for CN '" + cn + "' and its root CA are valid and uptodate. Skipping automated renewal.");
+ return result;
+ }
+ }
+
+ 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 for CN '" + cn + "'.");
+
+ Log(LogWarning, "JsonRpcConnection")
+ << "Could not calculate fingerprint for the X509 certificate requested for CN '"
+ << cn << "'.";
+
+ 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]);
+
+ result->Set("fingerprint_request", certFingerprint);
+
+ String requestDir = ApiListener::GetCertificateRequestsDir();
+ String requestPath = requestDir + "/" + certFingerprint + ".json";
+
+ result->Set("ca", CertificateToString(cacert));
+
+ JsonRpcConnection::Ptr client = origin->FromClient;
+
+ /* If we already have a signed certificate request, send it to the client. */
+ if (Utility::PathExists(requestPath)) {
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
+
+ String certResponse = request->Get("cert_response");
+
+ if (!certResponse.IsEmpty()) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "Sending certificate response for CN '" << cn
+ << "' to endpoint '" << client->GetIdentity() << "'.";
+
+ result->Set("cert", certResponse);
+ result->Set("status_code", 0);
+
+ Dictionary::Ptr message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "pki::UpdateCertificate" },
+ { "params", result }
+ });
+ client->SendMessage(message);
+
+ return result;
+ }
+ } else if (Utility::PathExists(requestDir + "/" + certFingerprint + ".removed")) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "Certificate for CN " << cn << " has been removed. Ignoring signing request.";
+ result->Set("status_code", 1);
+ result->Set("error", "Ticket for CN " + cn + " declined by administrator.");
+ return result;
+ }
+
+ std::shared_ptr<X509> newcert;
+ Dictionary::Ptr message;
+ String ticket;
+
+ /* Check whether we are a signing instance or we
+ * must delay the signing request.
+ */
+ if (!Utility::PathExists(GetIcingaCADir() + "/ca.key"))
+ goto delayed_request;
+
+ if (!signedByCA) {
+ String salt = listener->GetTicketSalt();
+
+ ticket = params->Get("ticket");
+
+ // Auto-signing is disabled: Client did not include a ticket in its request.
+ if (ticket.IsEmpty()) {
+ Log(LogNotice, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn
+ << "': No ticket included, skipping auto-signing and waiting for on-demand signing approval.";
+
+ goto delayed_request;
+ }
+
+ // Auto-signing is disabled: no TicketSalt
+ if (salt.IsEmpty()) {
+ Log(LogNotice, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn
+ << "': This instance is the signing master for the Icinga CA."
+ << " The 'ticket_salt' attribute in the 'api' feature is not set."
+ << " Not signing the request. Please check the docs.";
+
+ goto delayed_request;
+ }
+
+ String realTicket = PBKDF2_SHA1(cn, salt, 50000);
+
+ Log(LogDebug, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn << "': Comparing received ticket '"
+ << ticket << "' with calculated ticket '" << realTicket << "'.";
+
+ if (!Utility::ComparePasswords(ticket, realTicket)) {
+ Log(LogWarning, "JsonRpcConnection")
+ << "Ticket '" << ticket << "' for CN '" << cn << "' is invalid.";
+
+ result->Set("status_code", 1);
+ result->Set("error", "Invalid ticket for CN '" + cn + "'.");
+ return result;
+ }
+ }
+
+ newcert = listener->RenewCert(cert);
+
+ if (!newcert) {
+ goto delayed_request;
+ }
+
+ /* Send the signed certificate update. */
+ Log(LogInformation, "JsonRpcConnection")
+ << "Sending certificate response for CN '" << cn << "' to endpoint '"
+ << client->GetIdentity() << "'" << (!ticket.IsEmpty() ? " (auto-signing ticket)" : "" ) << ".";
+
+ result->Set("cert", CertificateToString(newcert));
+
+ result->Set("status_code", 0);
+
+ message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "pki::UpdateCertificate" },
+ { "params", result }
+ });
+ client->SendMessage(message);
+
+ return result;
+
+delayed_request:
+ /* Send a delayed certificate signing request. */
+ Utility::MkDirP(requestDir, 0700);
+
+ Dictionary::Ptr request = new Dictionary({
+ { "cert_request", CertificateToString(cert) },
+ { "ticket", params->Get("ticket") }
+ });
+
+ if (requestorCA) {
+ request->Set("requestor_ca", CertificateToString(requestorCA));
+ }
+
+ Utility::SaveJsonFile(requestPath, 0600, request);
+
+ JsonRpcConnection::SendCertificateRequest(nullptr, origin, requestPath);
+
+ result->Set("status_code", 2);
+ result->Set("error", "Certificate request for CN '" + cn + "' is pending. Waiting for approval from the parent Icinga instance.");
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn << "' is pending. Waiting for approval.";
+
+ if (origin) {
+ auto client (origin->FromClient);
+
+ if (client && !client->GetEndpoint()) {
+ client->Disconnect();
+ }
+ }
+
+ return result;
+}
+
+void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const MessageOrigin::Ptr& origin, const String& path)
+{
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "pki::RequestCertificate");
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr params = new Dictionary();
+ message->Set("params", params);
+
+ /* Path is empty if this is our own request. */
+ if (path.IsEmpty()) {
+ {
+ Log msg (LogInformation, "JsonRpcConnection");
+ msg << "Requesting new certificate for this Icinga instance";
+
+ if (aclient) {
+ msg << " from endpoint '" << aclient->GetIdentity() << "'";
+ }
+
+ msg << ".";
+ }
+
+ String ticketPath = ApiListener::GetCertsDir() + "/ticket";
+
+ std::ifstream fp(ticketPath.CStr());
+ String ticket((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
+ fp.close();
+
+ params->Set("ticket", ticket);
+ } else {
+ Dictionary::Ptr request = Utility::LoadJsonFile(path);
+
+ if (request->Contains("cert_response"))
+ return;
+
+ request->CopyTo(params);
+ }
+
+ /* Send the request to a) the connected client
+ * or b) the local zone and all parents.
+ */
+ if (aclient)
+ aclient->SendMessage(message);
+ else
+ listener->RelayMessage(origin, Zone::GetLocalZone(), message, false);
+}
+
+Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ if (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)) {
+ Log(LogWarning, "ClusterEvents")
+ << "Discarding 'update certificate' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ String ca = params->Get("ca");
+ String cert = params->Get("cert");
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ std::shared_ptr<X509> oldCert = GetX509Certificate(listener->GetDefaultCertPath());
+ std::shared_ptr<X509> newCert = StringToCertificate(cert);
+
+ String cn = GetCertificateCN(newCert);
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Received certificate update message for CN '" << cn << "'";
+
+ /* Check if this is a certificate update for a subordinate instance. */
+ std::shared_ptr<EVP_PKEY> oldKey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(oldCert.get()), EVP_PKEY_free);
+ std::shared_ptr<EVP_PKEY> newKey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(newCert.get()), EVP_PKEY_free);
+
+ if (X509_NAME_cmp(X509_get_subject_name(oldCert.get()), X509_get_subject_name(newCert.get())) != 0 ||
+ EVP_PKEY_cmp(oldKey.get(), newKey.get()) != 1) {
+ String certFingerprint = params->Get("fingerprint_request");
+
+ /* Validate the fingerprint format. */
+ boost::regex expr("^[0-9a-f]+$");
+
+ if (!boost::regex_match(certFingerprint.GetData(), expr)) {
+ Log(LogWarning, "JsonRpcConnection")
+ << "Endpoint '" << origin->FromClient->GetIdentity() << "' sent an invalid certificate fingerprint: '"
+ << certFingerprint << "' for CN '" << cn << "'.";
+ return Empty;
+ }
+
+ String requestDir = ApiListener::GetCertificateRequestsDir();
+ String requestPath = requestDir + "/" + certFingerprint + ".json";
+
+ /* Save the received signed certificate request to disk. */
+ if (Utility::PathExists(requestPath)) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "Saved certificate update for CN '" << cn << "'";
+
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
+ request->Set("cert_response", cert);
+ Utility::SaveJsonFile(requestPath, 0644, request);
+ }
+
+ return Empty;
+ }
+
+ /* Update CA certificate. */
+ String caPath = listener->GetDefaultCaPath();
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Updating CA certificate in '" << caPath << "'.";
+
+ AtomicFile::Write(caPath, 0644, ca);
+
+ /* Update signed certificate. */
+ String certPath = listener->GetDefaultCertPath();
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Updating client certificate for CN '" << cn << "' in '" << certPath << "'.";
+
+ AtomicFile::Write(certPath, 0644, cert);
+
+ /* Remove ticket for successful signing request. */
+ String ticketPath = ApiListener::GetCertsDir() + "/ticket";
+
+ Utility::Remove(ticketPath);
+
+ /* Update the certificates at runtime and reconnect all endpoints. */
+ Log(LogInformation, "JsonRpcConnection")
+ << "Updating the client certificate for CN '" << cn << "' at runtime and reconnecting the endpoints.";
+
+ listener->UpdateSSLContext();
+
+ return Empty;
+}