1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
2 
3 #include "remote/jsonrpcconnection.hpp"
4 #include "remote/apilistener.hpp"
5 #include "remote/apifunction.hpp"
6 #include "remote/jsonrpc.hpp"
7 #include "base/configtype.hpp"
8 #include "base/objectlock.hpp"
9 #include "base/utility.hpp"
10 #include "base/logger.hpp"
11 #include "base/exception.hpp"
12 #include "base/convert.hpp"
13 #include <boost/thread/once.hpp>
14 #include <boost/regex.hpp>
15 #include <fstream>
16 #include <openssl/ssl.h>
17 #include <openssl/x509.h>
18 
19 using namespace icinga;
20 
21 static Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
22 REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler);
23 static Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
24 REGISTER_APIFUNCTION(UpdateCertificate, pki, &UpdateCertificateHandler);
25 
RequestCertificateHandler(const MessageOrigin::Ptr & origin,const Dictionary::Ptr & params)26 Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
27 {
28 	String certText = params->Get("cert_request");
29 
30 	std::shared_ptr<X509> cert;
31 
32 	Dictionary::Ptr result = new Dictionary();
33 
34 	/* Use the presented client certificate if not provided. */
35 	if (certText.IsEmpty()) {
36 		auto stream (origin->FromClient->GetStream());
37 		cert = stream->next_layer().GetPeerCertificate();
38 	} else {
39 		cert = StringToCertificate(certText);
40 	}
41 
42 	if (!cert) {
43 		Log(LogWarning, "JsonRpcConnection") << "No certificate or CSR received";
44 
45 		result->Set("status_code", 1);
46 		result->Set("error", "No certificate or CSR received.");
47 
48 		return result;
49 	}
50 
51 	ApiListener::Ptr listener = ApiListener::GetInstance();
52 	std::shared_ptr<X509> cacert = GetX509Certificate(listener->GetDefaultCaPath());
53 
54 	String cn = GetCertificateCN(cert);
55 
56 	bool signedByCA = false;
57 
58 	{
59 		Log logmsg(LogInformation, "JsonRpcConnection");
60 		logmsg << "Received certificate request for CN '" << cn << "'";
61 
62 		try {
63 			signedByCA = VerifyCertificate(cacert, cert, listener->GetCrlPath());
64 			if (!signedByCA) {
65 				logmsg << " not";
66 			}
67 			logmsg << " signed by our CA.";
68 		} catch (const std::exception &ex) {
69 			logmsg << " not signed by our CA";
70 			if (const unsigned long *openssl_code = boost::get_error_info<errinfo_openssl_error>(ex)) {
71 				logmsg << ": " << X509_verify_cert_error_string(long(*openssl_code)) << " (code " << *openssl_code << ")";
72 			} else {
73 				logmsg << ".";
74 			}
75 		}
76 	}
77 
78 	if (signedByCA) {
79 		time_t now;
80 		time(&now);
81 
82 		/* auto-renew all certificates which were created before 2017 to force an update of the CA,
83 		 * because Icinga versions older than 2.4 sometimes create certificates with an invalid
84 		 * serial number. */
85 		time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */
86 		time_t renewalStart = now + 30 * 24 * 60 * 60;
87 
88 		if (X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1) {
89 
90 			Log(LogInformation, "JsonRpcConnection")
91 				<< "The certificate for CN '" << cn << "' is valid and uptodate. Skipping automated renewal.";
92 			result->Set("status_code", 1);
93 			result->Set("error", "The certificate for CN '" + cn + "' is valid and uptodate. Skipping automated renewal.");
94 			return result;
95 		}
96 	}
97 
98 	unsigned int n;
99 	unsigned char digest[EVP_MAX_MD_SIZE];
100 
101 	if (!X509_digest(cert.get(), EVP_sha256(), digest, &n)) {
102 		result->Set("status_code", 1);
103 		result->Set("error", "Could not calculate fingerprint for the X509 certificate for CN '" + cn + "'.");
104 
105 		Log(LogWarning, "JsonRpcConnection")
106 			<< "Could not calculate fingerprint for the X509 certificate requested for CN '"
107 			<< cn << "'.";
108 
109 		return result;
110 	}
111 
112 	char certFingerprint[EVP_MAX_MD_SIZE*2+1];
113 	for (unsigned int i = 0; i < n; i++)
114 		sprintf(certFingerprint + 2 * i, "%02x", digest[i]);
115 
116 	result->Set("fingerprint_request", certFingerprint);
117 
118 	String requestDir = ApiListener::GetCertificateRequestsDir();
119 	String requestPath = requestDir + "/" + certFingerprint + ".json";
120 
121 	result->Set("ca", CertificateToString(cacert));
122 
123 	JsonRpcConnection::Ptr client = origin->FromClient;
124 
125 	/* If we already have a signed certificate request, send it to the client. */
126 	if (Utility::PathExists(requestPath)) {
127 		Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
128 
129 		String certResponse = request->Get("cert_response");
130 
131 		if (!certResponse.IsEmpty()) {
132 			Log(LogInformation, "JsonRpcConnection")
133 				<< "Sending certificate response for CN '" << cn
134 				<< "' to endpoint '" << client->GetIdentity() << "'.";
135 
136 			result->Set("cert", certResponse);
137 			result->Set("status_code", 0);
138 
139 			Dictionary::Ptr message = new Dictionary({
140 				{ "jsonrpc", "2.0" },
141 				{ "method", "pki::UpdateCertificate" },
142 				{ "params", result }
143 			});
144 			client->SendMessage(message);
145 
146 			return result;
147 		}
148 	} else if (Utility::PathExists(requestDir + "/" + certFingerprint + ".removed")) {
149 		Log(LogInformation, "JsonRpcConnection")
150 			<< "Certificate for CN " << cn << " has been removed. Ignoring signing request.";
151 		result->Set("status_code", 1);
152 		result->Set("error", "Ticket for CN " + cn + " declined by administrator.");
153 		return result;
154 	}
155 
156 	std::shared_ptr<X509> newcert;
157 	std::shared_ptr<EVP_PKEY> pubkey;
158 	X509_NAME *subject;
159 	Dictionary::Ptr message;
160 	String ticket;
161 
162 	/* Check whether we are a signing instance or we
163 	 * must delay the signing request.
164 	 */
165 	if (!Utility::PathExists(GetIcingaCADir() + "/ca.key"))
166 		goto delayed_request;
167 
168 	if (!signedByCA) {
169 		String salt = listener->GetTicketSalt();
170 
171 		ticket = params->Get("ticket");
172 
173 		// Auto-signing is disabled: Client did not include a ticket in its request.
174 		if (ticket.IsEmpty()) {
175 			Log(LogNotice, "JsonRpcConnection")
176 				<< "Certificate request for CN '" << cn
177 				<< "': No ticket included, skipping auto-signing and waiting for on-demand signing approval.";
178 
179 			goto delayed_request;
180 		}
181 
182 		// Auto-signing is disabled: no TicketSalt
183 		if (salt.IsEmpty()) {
184 			Log(LogNotice, "JsonRpcConnection")
185 				<< "Certificate request for CN '" << cn
186 				<< "': This instance is the signing master for the Icinga CA."
187 				<< " The 'ticket_salt' attribute in the 'api' feature is not set."
188 				<< " Not signing the request. Please check the docs.";
189 
190 			goto delayed_request;
191 		}
192 
193 		String realTicket = PBKDF2_SHA1(cn, salt, 50000);
194 
195 		Log(LogDebug, "JsonRpcConnection")
196 			<< "Certificate request for CN '" << cn << "': Comparing received ticket '"
197 			<< ticket << "' with calculated ticket '" << realTicket << "'.";
198 
199 		if (ticket != realTicket) {
200 			Log(LogWarning, "JsonRpcConnection")
201 				<< "Ticket '" << ticket << "' for CN '" << cn << "' is invalid.";
202 
203 			result->Set("status_code", 1);
204 			result->Set("error", "Invalid ticket for CN '" + cn + "'.");
205 			return result;
206 		}
207 	}
208 
209 	pubkey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(cert.get()), EVP_PKEY_free);
210 	subject = X509_get_subject_name(cert.get());
211 
212 	newcert = CreateCertIcingaCA(pubkey.get(), subject);
213 
214 	/* verify that the new cert matches the CA we're using for the ApiListener;
215 	 * this ensures that the CA we have in /var/lib/icinga2/ca matches the one
216 	 * we're using for cluster connections (there's no point in sending a client
217 	 * a certificate it wouldn't be able to use to connect to us anyway) */
218 	try {
219 		if (!VerifyCertificate(cacert, newcert, listener->GetCrlPath())) {
220 			Log(LogWarning, "JsonRpcConnection")
221 				<< "The CA in '" << listener->GetDefaultCaPath() << "' does not match the CA which Icinga uses "
222 				<< "for its own cluster connections. This is most likely a configuration problem.";
223 			goto delayed_request;
224 		}
225 	} catch (const std::exception&) { } /* Swallow the exception on purpose, cacert will never be a non-CA certificate. */
226 
227 	/* Send the signed certificate update. */
228 	Log(LogInformation, "JsonRpcConnection")
229 		<< "Sending certificate response for CN '" << cn << "' to endpoint '"
230 		<< client->GetIdentity() << "'" << (!ticket.IsEmpty() ? " (auto-signing ticket)" : "" ) << ".";
231 
232 	result->Set("cert", CertificateToString(newcert));
233 
234 	result->Set("status_code", 0);
235 
236 	message = new Dictionary({
237 		{ "jsonrpc", "2.0" },
238 		{ "method", "pki::UpdateCertificate" },
239 		{ "params", result }
240 	});
241 	client->SendMessage(message);
242 
243 	return result;
244 
245 delayed_request:
246 	/* Send a delayed certificate signing request. */
247 	Utility::MkDirP(requestDir, 0700);
248 
249 	Dictionary::Ptr request = new Dictionary({
250 		{ "cert_request", CertificateToString(cert) },
251 		{ "ticket", params->Get("ticket") }
252 	});
253 
254 	Utility::SaveJsonFile(requestPath, 0600, request);
255 
256 	JsonRpcConnection::SendCertificateRequest(nullptr, origin, requestPath);
257 
258 	result->Set("status_code", 2);
259 	result->Set("error", "Certificate request for CN '" + cn + "' is pending. Waiting for approval from the parent Icinga instance.");
260 
261 	Log(LogInformation, "JsonRpcConnection")
262 		<< "Certificate request for CN '" << cn << "' is pending. Waiting for approval.";
263 
264 	if (origin) {
265 		auto client (origin->FromClient);
266 
267 		if (client && !client->GetEndpoint()) {
268 			client->Disconnect();
269 		}
270 	}
271 
272 	return result;
273 }
274 
SendCertificateRequest(const JsonRpcConnection::Ptr & aclient,const MessageOrigin::Ptr & origin,const String & path)275 void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const MessageOrigin::Ptr& origin, const String& path)
276 {
277 	Dictionary::Ptr message = new Dictionary();
278 	message->Set("jsonrpc", "2.0");
279 	message->Set("method", "pki::RequestCertificate");
280 
281 	ApiListener::Ptr listener = ApiListener::GetInstance();
282 
283 	if (!listener)
284 		return;
285 
286 	Dictionary::Ptr params = new Dictionary();
287 	message->Set("params", params);
288 
289 	/* Path is empty if this is our own request. */
290 	if (path.IsEmpty()) {
291 		String ticketPath = ApiListener::GetCertsDir() + "/ticket";
292 
293 		std::ifstream fp(ticketPath.CStr());
294 		String ticket((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
295 		fp.close();
296 
297 		params->Set("ticket", ticket);
298 	} else {
299 		Dictionary::Ptr request = Utility::LoadJsonFile(path);
300 
301 		if (request->Contains("cert_response"))
302 			return;
303 
304 		params->Set("cert_request", request->Get("cert_request"));
305 		params->Set("ticket", request->Get("ticket"));
306 	}
307 
308 	/* Send the request to a) the connected client
309 	 * or b) the local zone and all parents.
310 	 */
311 	if (aclient)
312 		aclient->SendMessage(message);
313 	else
314 		listener->RelayMessage(origin, Zone::GetLocalZone(), message, false);
315 }
316 
UpdateCertificateHandler(const MessageOrigin::Ptr & origin,const Dictionary::Ptr & params)317 Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
318 {
319 	if (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)) {
320 		Log(LogWarning, "ClusterEvents")
321 			<< "Discarding 'update certificate' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
322 
323 		return Empty;
324 	}
325 
326 	String ca = params->Get("ca");
327 	String cert = params->Get("cert");
328 
329 	ApiListener::Ptr listener = ApiListener::GetInstance();
330 
331 	if (!listener)
332 		return Empty;
333 
334 	std::shared_ptr<X509> oldCert = GetX509Certificate(listener->GetDefaultCertPath());
335 	std::shared_ptr<X509> newCert = StringToCertificate(cert);
336 
337 	String cn = GetCertificateCN(newCert);
338 
339 	Log(LogInformation, "JsonRpcConnection")
340 		<< "Received certificate update message for CN '" << cn << "'";
341 
342 	/* Check if this is a certificate update for a subordinate instance. */
343 	std::shared_ptr<EVP_PKEY> oldKey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(oldCert.get()), EVP_PKEY_free);
344 	std::shared_ptr<EVP_PKEY> newKey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(newCert.get()), EVP_PKEY_free);
345 
346 	if (X509_NAME_cmp(X509_get_subject_name(oldCert.get()), X509_get_subject_name(newCert.get())) != 0 ||
347 		EVP_PKEY_cmp(oldKey.get(), newKey.get()) != 1) {
348 		String certFingerprint = params->Get("fingerprint_request");
349 
350 		/* Validate the fingerprint format. */
351 		boost::regex expr("^[0-9a-f]+$");
352 
353 		if (!boost::regex_match(certFingerprint.GetData(), expr)) {
354 			Log(LogWarning, "JsonRpcConnection")
355 				<< "Endpoint '" << origin->FromClient->GetIdentity() << "' sent an invalid certificate fingerprint: '"
356 				<< certFingerprint << "' for CN '" << cn << "'.";
357 			return Empty;
358 		}
359 
360 		String requestDir = ApiListener::GetCertificateRequestsDir();
361 		String requestPath = requestDir + "/" + certFingerprint + ".json";
362 
363 		/* Save the received signed certificate request to disk. */
364 		if (Utility::PathExists(requestPath)) {
365 			Log(LogInformation, "JsonRpcConnection")
366 				<< "Saved certificate update for CN '" << cn << "'";
367 
368 			Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
369 			request->Set("cert_response", cert);
370 			Utility::SaveJsonFile(requestPath, 0644, request);
371 		}
372 
373 		return Empty;
374 	}
375 
376 	/* Update CA certificate. */
377 	String caPath = listener->GetDefaultCaPath();
378 
379 	Log(LogInformation, "JsonRpcConnection")
380 		<< "Updating CA certificate in '" << caPath << "'.";
381 
382 	std::fstream cafp;
383 	String tempCaPath = Utility::CreateTempFile(caPath + ".XXXXXX", 0644, cafp);
384 	cafp << ca;
385 	cafp.close();
386 
387 	Utility::RenameFile(tempCaPath, caPath);
388 
389 	/* Update signed certificate. */
390 	String certPath = listener->GetDefaultCertPath();
391 
392 	Log(LogInformation, "JsonRpcConnection")
393 		<< "Updating client certificate for CN '" << cn << "' in '" << certPath << "'.";
394 
395 	std::fstream certfp;
396 	String tempCertPath = Utility::CreateTempFile(certPath + ".XXXXXX", 0644, certfp);
397 	certfp << cert;
398 	certfp.close();
399 
400 	Utility::RenameFile(tempCertPath, certPath);
401 
402 	/* Remove ticket for successful signing request. */
403 	String ticketPath = ApiListener::GetCertsDir() + "/ticket";
404 
405 	Utility::Remove(ticketPath);
406 
407 	/* Update the certificates at runtime and reconnect all endpoints. */
408 	Log(LogInformation, "JsonRpcConnection")
409 		<< "Updating the client certificate for CN '" << cn << "' at runtime and reconnecting the endpoints.";
410 
411 	listener->UpdateSSLContext();
412 
413 	return Empty;
414 }
415