]> granicus.if.org Git - icinga2/blob - lib/remote/jsonrpcconnection-pki.cpp
Fix nullptr deref in cluster events
[icinga2] / lib / remote / jsonrpcconnection-pki.cpp
1 /******************************************************************************
2  * Icinga 2                                                                   *
3  * Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/)  *
4  *                                                                            *
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.                     *
9  *                                                                            *
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.                               *
14  *                                                                            *
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  ******************************************************************************/
19
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>
32 #include <fstream>
33
34 using namespace icinga;
35
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);
40
41 Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
42 {
43         String certText = params->Get("cert_request");
44
45         boost::shared_ptr<X509> cert;
46
47         Dictionary::Ptr result = new Dictionary();
48
49         /* Use the presented client certificate if not provided. */
50         if (certText.IsEmpty())
51                 cert = origin->FromClient->GetStream()->GetPeerCertificate();
52         else
53                 cert = StringToCertificate(certText);
54
55         ApiListener::Ptr listener = ApiListener::GetInstance();
56         boost::shared_ptr<X509> cacert = GetX509Certificate(listener->GetDefaultCaPath());
57
58         String cn = GetCertificateCN(cert);
59
60         bool signedByCA = VerifyCertificate(cacert, cert);
61
62         Log(LogInformation, "JsonRpcConnection")
63             << "Received certificate request for CN '" << cn << "'"
64             << (signedByCA ? "" : " not") << " signed by our CA.";
65
66         if (signedByCA) {
67                 time_t now;
68                 time(&now);
69
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
72                  * serial number. */
73                 time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */
74                 time_t renewalStart = now + 30 * 24 * 60 * 60;
75
76                 if (X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1) {
77
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.");
82                         return result;
83                 }
84         }
85
86         unsigned int n;
87         unsigned char digest[EVP_MAX_MD_SIZE];
88
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 + "'.");
92
93                 Log(LogWarning, "JsonRpcConnection")
94                     << "Could not calculate fingerprint for the X509 certificate requested for CN '"
95                     << cn << "'.";
96
97                 return result;
98         }
99
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]);
103
104         result->Set("fingerprint_request", certFingerprint);
105
106         String requestDir = ApiListener::GetCertificateRequestsDir();
107         String requestPath = requestDir + "/" + certFingerprint + ".json";
108
109         result->Set("ca", CertificateToString(cacert));
110
111         JsonRpcConnection::Ptr client = origin->FromClient;
112
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);
116
117                 String certResponse = request->Get("cert_response");
118
119                 if (!certResponse.IsEmpty()) {
120                         Log(LogInformation, "JsonRpcConnection")
121                             << "Sending certificate response for CN '" << cn
122                             << "' to endpoint '" << client->GetIdentity() << "'.";
123
124                         result->Set("cert", certResponse);
125                         result->Set("status_code", 0);
126
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);
132
133                         return result;
134                 }
135         }
136
137         boost::shared_ptr<X509> newcert;
138         boost::shared_ptr<EVP_PKEY> pubkey;
139         X509_NAME *subject;
140         Dictionary::Ptr message;
141         String ticket;
142
143         /* Check whether we are a signing instance or we
144          * must delay the signing request.
145          */
146         if (!Utility::PathExists(GetIcingaCADir() + "/ca.key"))
147                 goto delayed_request;
148
149         if (!signedByCA) {
150                 String salt = listener->GetTicketSalt();
151
152                 ticket = params->Get("ticket");
153
154                 /* Auto-signing is disabled by either a) no TicketSalt
155                  * or b) the client did not include a ticket in its request.
156                  */
157                 if (salt.IsEmpty() || ticket.IsEmpty())
158                         goto delayed_request;
159
160                 String realTicket = PBKDF2_SHA1(cn, salt, 50000);
161
162                 if (ticket != realTicket) {
163                         Log(LogWarning, "JsonRpcConnection")
164                             << "Ticket for CN '" << cn << "' is invalid.";
165
166                         result->Set("status_code", 1);
167                         result->Set("error", "Invalid ticket for CN '" + cn + "'.");
168                         return result;
169                 }
170         }
171
172         pubkey = boost::shared_ptr<EVP_PKEY>(X509_get_pubkey(cert.get()), EVP_PKEY_free);
173         subject = X509_get_subject_name(cert.get());
174
175         newcert = CreateCertIcingaCA(pubkey.get(), subject);
176
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;
186         }
187
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)" : "" ) << ".";
192
193         result->Set("cert", CertificateToString(newcert));
194
195         result->Set("status_code", 0);
196
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);
202
203         return result;
204
205 delayed_request:
206         /* Send a delayed certificate signing request. */
207         Utility::MkDirP(requestDir, 0700);
208
209         Dictionary::Ptr request = new Dictionary();
210         request->Set("cert_request", CertificateToString(cert));
211         request->Set("ticket", params->Get("ticket"));
212
213         Utility::SaveJsonFile(requestPath, 0600, request);
214
215         JsonRpcConnection::SendCertificateRequest(JsonRpcConnection::Ptr(), origin, requestPath);
216
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.");
219
220         Log(LogInformation, "JsonRpcConnection")
221             << "Certificate request for CN '" << cn << "' is pending. Waiting for approval.";
222
223         return result;
224 }
225
226 void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const MessageOrigin::Ptr& origin, const String& path)
227 {
228         Dictionary::Ptr message = new Dictionary();
229         message->Set("jsonrpc", "2.0");
230         message->Set("method", "pki::RequestCertificate");
231
232         ApiListener::Ptr listener = ApiListener::GetInstance();
233
234         if (!listener)
235                 return;
236
237         Dictionary::Ptr params = new Dictionary();
238         message->Set("params", params);
239
240         /* Path is empty if this is our own request. */
241         if (path.IsEmpty()) {
242                 String ticketPath = ApiListener::GetCertsDir() + "/ticket";
243
244                 std::ifstream fp(ticketPath.CStr());
245                 String ticket((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
246                 fp.close();
247
248                 params->Set("ticket", ticket);
249         } else {
250                 Dictionary::Ptr request = Utility::LoadJsonFile(path);
251
252                 if (request->Contains("cert_response"))
253                         return;
254
255                 params->Set("cert_request", request->Get("cert_request"));
256                 params->Set("ticket", request->Get("ticket"));
257         }
258
259         /* Send the request to a) the connected client
260          * or b) the local zone and all parents.
261          */
262         if (aclient)
263                 JsonRpc::SendMessage(aclient->GetStream(), message);
264         else
265                 listener->RelayMessage(origin, Zone::GetLocalZone(), message, false);
266 }
267
268 Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
269 {
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).";
273
274                 return Empty;
275         }
276
277         String ca = params->Get("ca");
278         String cert = params->Get("cert");
279
280         ApiListener::Ptr listener = ApiListener::GetInstance();
281
282         if (!listener)
283                 return Empty;
284
285         boost::shared_ptr<X509> oldCert = GetX509Certificate(listener->GetDefaultCertPath());
286         boost::shared_ptr<X509> newCert = StringToCertificate(cert);
287
288         String cn = GetCertificateCN(newCert);
289
290         Log(LogInformation, "JsonRpcConnection")
291             << "Received certificate update message for CN '" << cn << "'";
292
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);
296
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");
300
301                 /* Validate the fingerprint format. */
302                 boost::regex expr("^[0-9a-f]+$");
303
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 << "'.";
308                         return Empty;
309                 }
310
311                 String requestDir = ApiListener::GetCertificateRequestsDir();
312                 String requestPath = requestDir + "/" + certFingerprint + ".json";
313
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 << "'";
318
319                         Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
320                         request->Set("cert_response", cert);
321                         Utility::SaveJsonFile(requestPath, 0644, request);
322                 }
323
324                 return Empty;
325         }
326
327         /* Update CA certificate. */
328         String caPath = listener->GetDefaultCaPath();
329
330         Log(LogInformation, "JsonRpcConnection")
331             << "Updating CA certificate in '" << caPath << "'.";
332
333         std::fstream cafp;
334         String tempCaPath = Utility::CreateTempFile(caPath + ".XXXXXX", 0644, cafp);
335         cafp << ca;
336         cafp.close();
337
338 #ifdef _WIN32
339         _unlink(caPath.CStr());
340 #endif /* _WIN32 */
341
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));
347         }
348
349         /* Update signed certificate. */
350         String certPath = listener->GetDefaultCertPath();
351
352         Log(LogInformation, "JsonRpcConnection")
353             << "Updating client certificate for CN '" << cn << "' in '" << certPath << "'.";
354
355         std::fstream certfp;
356         String tempCertPath = Utility::CreateTempFile(certPath + ".XXXXXX", 0644, certfp);
357         certfp << cert;
358         certfp.close();
359
360 #ifdef _WIN32
361         _unlink(certPath.CStr());
362 #endif /* _WIN32 */
363
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));
369         }
370
371         /* Remove ticket for successful signing request. */
372         String ticketPath = ApiListener::GetCertsDir() + "/ticket";
373
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));
379         }
380
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.";
384
385         listener->UpdateSSLContext();
386
387         return Empty;
388 }