1 /******************************************************************************
3 * Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/) *
5 * This program is free software; you can redistribute it and/or *
6 * modify it under the terms of the GNU General Public License *
7 * as published by the Free Software Foundation; either version 2 *
8 * of the License, or (at your option) any later version. *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the Free Software Foundation *
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
18 ******************************************************************************/
20 #include "remote/jsonrpcconnection.hpp"
21 #include "remote/apilistener.hpp"
22 #include "remote/apifunction.hpp"
23 #include "remote/jsonrpc.hpp"
24 #include "base/configtype.hpp"
25 #include "base/objectlock.hpp"
26 #include "base/utility.hpp"
27 #include "base/logger.hpp"
28 #include "base/exception.hpp"
29 #include "base/convert.hpp"
30 #include <boost/thread/once.hpp>
31 #include <boost/regex.hpp>
34 using namespace icinga;
36 static Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
37 REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler);
38 static Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
39 REGISTER_APIFUNCTION(UpdateCertificate, pki, &UpdateCertificateHandler);
41 Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
43 String certText = params->Get("cert_request");
45 boost::shared_ptr<X509> cert;
47 Dictionary::Ptr result = new Dictionary();
49 /* Use the presented client certificate if not provided. */
50 if (certText.IsEmpty())
51 cert = origin->FromClient->GetStream()->GetPeerCertificate();
53 cert = StringToCertificate(certText);
55 ApiListener::Ptr listener = ApiListener::GetInstance();
56 boost::shared_ptr<X509> cacert = GetX509Certificate(listener->GetDefaultCaPath());
58 String cn = GetCertificateCN(cert);
60 bool signedByCA = VerifyCertificate(cacert, cert);
62 Log(LogInformation, "JsonRpcConnection")
63 << "Received certificate request for CN '" << cn << "'"
64 << (signedByCA ? "" : " not") << " signed by our CA.";
70 /* auto-renew all certificates which were created before 2017 to force an update of the CA,
71 * because Icinga versions older than 2.4 sometimes create certificates with an invalid
73 time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */
74 time_t renewalStart = now + 30 * 24 * 60 * 60;
76 if (X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1) {
78 Log(LogInformation, "JsonRpcConnection")
79 << "The certificate for CN '" << cn << "' cannot be renewed yet.";
80 result->Set("status_code", 1);
81 result->Set("error", "The certificate for CN '" + cn + "' cannot be renewed yet.");
87 unsigned char digest[EVP_MAX_MD_SIZE];
89 if (!X509_digest(cert.get(), EVP_sha256(), digest, &n)) {
90 result->Set("status_code", 1);
91 result->Set("error", "Could not calculate fingerprint for the X509 certificate for CN '" + cn + "'.");
93 Log(LogWarning, "JsonRpcConnection")
94 << "Could not calculate fingerprint for the X509 certificate requested for CN '"
100 char certFingerprint[EVP_MAX_MD_SIZE*2+1];
101 for (unsigned int i = 0; i < n; i++)
102 sprintf(certFingerprint + 2 * i, "%02x", digest[i]);
104 result->Set("fingerprint_request", certFingerprint);
106 String requestDir = ApiListener::GetCertificateRequestsDir();
107 String requestPath = requestDir + "/" + certFingerprint + ".json";
109 result->Set("ca", CertificateToString(cacert));
111 JsonRpcConnection::Ptr client = origin->FromClient;
113 /* If we already have a signed certificate request, send it to the client. */
114 if (Utility::PathExists(requestPath)) {
115 Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
117 String certResponse = request->Get("cert_response");
119 if (!certResponse.IsEmpty()) {
120 Log(LogInformation, "JsonRpcConnection")
121 << "Sending certificate response for CN '" << cn
122 << "' to endpoint '" << client->GetIdentity() << "'.";
124 result->Set("cert", certResponse);
125 result->Set("status_code", 0);
127 Dictionary::Ptr message = new Dictionary();
128 message->Set("jsonrpc", "2.0");
129 message->Set("method", "pki::UpdateCertificate");
130 message->Set("params", result);
131 JsonRpc::SendMessage(client->GetStream(), message);
137 boost::shared_ptr<X509> newcert;
138 boost::shared_ptr<EVP_PKEY> pubkey;
140 Dictionary::Ptr message;
143 /* Check whether we are a signing instance or we
144 * must delay the signing request.
146 if (!Utility::PathExists(GetIcingaCADir() + "/ca.key"))
147 goto delayed_request;
150 String salt = listener->GetTicketSalt();
152 ticket = params->Get("ticket");
154 /* Auto-signing is disabled by either a) no TicketSalt
155 * or b) the client did not include a ticket in its request.
157 if (salt.IsEmpty() || ticket.IsEmpty())
158 goto delayed_request;
160 String realTicket = PBKDF2_SHA1(cn, salt, 50000);
162 if (ticket != realTicket) {
163 Log(LogWarning, "JsonRpcConnection")
164 << "Ticket for CN '" << cn << "' is invalid.";
166 result->Set("status_code", 1);
167 result->Set("error", "Invalid ticket for CN '" + cn + "'.");
172 pubkey = boost::shared_ptr<EVP_PKEY>(X509_get_pubkey(cert.get()), EVP_PKEY_free);
173 subject = X509_get_subject_name(cert.get());
175 newcert = CreateCertIcingaCA(pubkey.get(), subject);
177 /* verify that the new cert matches the CA we're using for the ApiListener;
178 * this ensures that the CA we have in /var/lib/icinga2/ca matches the one
179 * we're using for cluster connections (there's no point in sending a client
180 * a certificate it wouldn't be able to use to connect to us anyway) */
181 if (!VerifyCertificate(cacert, newcert)) {
182 Log(LogWarning, "JsonRpcConnection")
183 << "The CA in '" << listener->GetDefaultCaPath() << "' does not match the CA which Icinga uses "
184 << "for its own cluster connections. This is most likely a configuration problem.";
185 goto delayed_request;
188 /* Send the signed certificate update. */
189 Log(LogInformation, "JsonRpcConnection")
190 << "Sending certificate response for CN '" << cn << "' to endpoint '"
191 << client->GetIdentity() << "'" << (!ticket.IsEmpty() ? " (auto-signing ticket)" : "" ) << ".";
193 result->Set("cert", CertificateToString(newcert));
195 result->Set("status_code", 0);
197 message = new Dictionary();
198 message->Set("jsonrpc", "2.0");
199 message->Set("method", "pki::UpdateCertificate");
200 message->Set("params", result);
201 JsonRpc::SendMessage(client->GetStream(), message);
206 /* Send a delayed certificate signing request. */
207 Utility::MkDirP(requestDir, 0700);
209 Dictionary::Ptr request = new Dictionary();
210 request->Set("cert_request", CertificateToString(cert));
211 request->Set("ticket", params->Get("ticket"));
213 Utility::SaveJsonFile(requestPath, 0600, request);
215 JsonRpcConnection::SendCertificateRequest(JsonRpcConnection::Ptr(), origin, requestPath);
217 result->Set("status_code", 2);
218 result->Set("error", "Certificate request for CN '" + cn + "' is pending. Waiting for approval from the parent Icinga instance.");
220 Log(LogInformation, "JsonRpcConnection")
221 << "Certificate request for CN '" << cn << "' is pending. Waiting for approval.";
226 void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const MessageOrigin::Ptr& origin, const String& path)
228 Dictionary::Ptr message = new Dictionary();
229 message->Set("jsonrpc", "2.0");
230 message->Set("method", "pki::RequestCertificate");
232 ApiListener::Ptr listener = ApiListener::GetInstance();
237 Dictionary::Ptr params = new Dictionary();
238 message->Set("params", params);
240 /* Path is empty if this is our own request. */
241 if (path.IsEmpty()) {
242 String ticketPath = ApiListener::GetCertsDir() + "/ticket";
244 std::ifstream fp(ticketPath.CStr());
245 String ticket((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
248 params->Set("ticket", ticket);
250 Dictionary::Ptr request = Utility::LoadJsonFile(path);
252 if (request->Contains("cert_response"))
255 params->Set("cert_request", request->Get("cert_request"));
256 params->Set("ticket", request->Get("ticket"));
259 /* Send the request to a) the connected client
260 * or b) the local zone and all parents.
263 JsonRpc::SendMessage(aclient->GetStream(), message);
265 listener->RelayMessage(origin, Zone::GetLocalZone(), message, false);
268 Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
270 if (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)) {
271 Log(LogWarning, "ClusterEvents")
272 << "Discarding 'update certificate' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
277 String ca = params->Get("ca");
278 String cert = params->Get("cert");
280 ApiListener::Ptr listener = ApiListener::GetInstance();
285 boost::shared_ptr<X509> oldCert = GetX509Certificate(listener->GetDefaultCertPath());
286 boost::shared_ptr<X509> newCert = StringToCertificate(cert);
288 String cn = GetCertificateCN(newCert);
290 Log(LogInformation, "JsonRpcConnection")
291 << "Received certificate update message for CN '" << cn << "'";
293 /* Check if this is a certificate update for a subordinate instance. */
294 boost::shared_ptr<EVP_PKEY> oldKey = boost::shared_ptr<EVP_PKEY>(X509_get_pubkey(oldCert.get()), EVP_PKEY_free);
295 boost::shared_ptr<EVP_PKEY> newKey = boost::shared_ptr<EVP_PKEY>(X509_get_pubkey(newCert.get()), EVP_PKEY_free);
297 if (X509_NAME_cmp(X509_get_subject_name(oldCert.get()), X509_get_subject_name(newCert.get())) != 0 ||
298 EVP_PKEY_cmp(oldKey.get(), newKey.get()) != 1) {
299 String certFingerprint = params->Get("fingerprint_request");
301 /* Validate the fingerprint format. */
302 boost::regex expr("^[0-9a-f]+$");
304 if (!boost::regex_match(certFingerprint.GetData(), expr)) {
305 Log(LogWarning, "JsonRpcConnection")
306 << "Endpoint '" << origin->FromClient->GetIdentity() << "' sent an invalid certificate fingerprint: '"
307 << certFingerprint << "' for CN '" << cn << "'.";
311 String requestDir = ApiListener::GetCertificateRequestsDir();
312 String requestPath = requestDir + "/" + certFingerprint + ".json";
314 /* Save the received signed certificate request to disk. */
315 if (Utility::PathExists(requestPath)) {
316 Log(LogInformation, "JsonRpcConnection")
317 << "Saved certificate update for CN '" << cn << "'";
319 Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
320 request->Set("cert_response", cert);
321 Utility::SaveJsonFile(requestPath, 0644, request);
327 /* Update CA certificate. */
328 String caPath = listener->GetDefaultCaPath();
330 Log(LogInformation, "JsonRpcConnection")
331 << "Updating CA certificate in '" << caPath << "'.";
334 String tempCaPath = Utility::CreateTempFile(caPath + ".XXXXXX", 0644, cafp);
339 _unlink(caPath.CStr());
342 if (rename(tempCaPath.CStr(), caPath.CStr()) < 0) {
343 BOOST_THROW_EXCEPTION(posix_error()
344 << boost::errinfo_api_function("rename")
345 << boost::errinfo_errno(errno)
346 << boost::errinfo_file_name(tempCaPath));
349 /* Update signed certificate. */
350 String certPath = listener->GetDefaultCertPath();
352 Log(LogInformation, "JsonRpcConnection")
353 << "Updating client certificate for CN '" << cn << "' in '" << certPath << "'.";
356 String tempCertPath = Utility::CreateTempFile(certPath + ".XXXXXX", 0644, certfp);
361 _unlink(certPath.CStr());
364 if (rename(tempCertPath.CStr(), certPath.CStr()) < 0) {
365 BOOST_THROW_EXCEPTION(posix_error()
366 << boost::errinfo_api_function("rename")
367 << boost::errinfo_errno(errno)
368 << boost::errinfo_file_name(tempCertPath));
371 /* Remove ticket for successful signing request. */
372 String ticketPath = ApiListener::GetCertsDir() + "/ticket";
374 if (unlink(ticketPath.CStr()) < 0 && errno != ENOENT) {
375 BOOST_THROW_EXCEPTION(posix_error()
376 << boost::errinfo_api_function("unlink")
377 << boost::errinfo_errno(errno)
378 << boost::errinfo_file_name(ticketPath));
381 /* Update the certificates at runtime and reconnect all endpoints. */
382 Log(LogInformation, "JsonRpcConnection")
383 << "Updating the client certificate for CN '" << cn << "' at runtime and reconnecting the endpoints.";
385 listener->UpdateSSLContext();