/* 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/configtype.hpp" #include "base/objectlock.hpp" #include "base/utility.hpp" #include "base/logger.hpp" #include "base/exception.hpp" #include "base/convert.hpp" #include #include #include #include #include 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 cert; Dictionary::Ptr result = new Dictionary(); /* Use the presented client certificate if not provided. */ if (certText.IsEmpty()) { auto stream (origin->FromClient->GetStream()); cert = stream->next_layer().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 cacert = GetX509Certificate(listener->GetDefaultCaPath()); String cn = GetCertificateCN(cert); bool signedByCA = VerifyCertificate(cacert, cert); Log(LogInformation, "JsonRpcConnection") << "Received certificate request for CN '" << cn << "'" << (signedByCA ? "" : " not") << " signed by our CA."; if (signedByCA) { time_t now; time(&now); /* auto-renew all certificates which were created before 2017 to force an update of the CA, * because Icinga versions older than 2.4 sometimes create certificates with an invalid * serial number. */ time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */ time_t renewalStart = now + 30 * 24 * 60 * 60; if (X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1) { Log(LogInformation, "JsonRpcConnection") << "The certificate for CN '" << cn << "' is valid and uptodate. Skipping automated renewal."; result->Set("status_code", 1); result->Set("error", "The certificate for CN '" + cn + "' is 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 newcert; std::shared_ptr pubkey; X509_NAME *subject; 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 (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; } } pubkey = std::shared_ptr(X509_get_pubkey(cert.get()), EVP_PKEY_free); subject = X509_get_subject_name(cert.get()); newcert = CreateCertIcingaCA(pubkey.get(), 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->GetDefaultCaPath() << "' does not match the CA which Icinga uses " << "for its own cluster connections. This is most likely a configuration problem."; 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") } }); 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."; 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()) { String ticketPath = ApiListener::GetCertsDir() + "/ticket"; std::ifstream fp(ticketPath.CStr()); String ticket((std::istreambuf_iterator(fp)), std::istreambuf_iterator()); fp.close(); params->Set("ticket", ticket); } else { Dictionary::Ptr request = Utility::LoadJsonFile(path); if (request->Contains("cert_response")) return; params->Set("cert_request", request->Get("cert_request")); params->Set("ticket", request->Get("ticket")); } /* 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 oldCert = GetX509Certificate(listener->GetDefaultCertPath()); std::shared_ptr 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 oldKey = std::shared_ptr(X509_get_pubkey(oldCert.get()), EVP_PKEY_free); std::shared_ptr newKey = std::shared_ptr(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 << "'."; std::fstream cafp; String tempCaPath = Utility::CreateTempFile(caPath + ".XXXXXX", 0644, cafp); cafp << ca; cafp.close(); Utility::RenameFile(tempCaPath, caPath); /* Update signed certificate. */ String certPath = listener->GetDefaultCertPath(); Log(LogInformation, "JsonRpcConnection") << "Updating client certificate for CN '" << cn << "' in '" << certPath << "'."; std::fstream certfp; String tempCertPath = Utility::CreateTempFile(certPath + ".XXXXXX", 0644, certfp); certfp << cert; certfp.close(); Utility::RenameFile(tempCertPath, certPath); /* 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; }