icinga2/lib/remote/jsonrpcconnection-pki.cpp
Markus Frosch 847f55bc8a ApiListener: Add external_ca flag to control renewal behavior
This will disable checking and renewing certificates with the Icinga master or other parents within the cluster, so it is up for the user to control it.

I named the flag non-specific to being able to control other occasions where a non Icinga CA should be treated differently.
2021-10-07 12:06:55 +02:00

421 lines
13 KiB
C++

/* 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 <boost/thread/once.hpp>
#include <boost/regex.hpp>
#include <fstream>
#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)
{
ApiListener::Ptr listener = ApiListener::GetInstance();
Dictionary::Ptr result = new Dictionary();
// When external_ca is configured, ignore any request via RPC.
if (listener->GetExternalCa()) {
return Empty;
}
String certText = params->Get("cert_request");
std::shared_ptr<X509> cert;
/* 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;
}
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 << " not signed by our CA";
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 << ".";
}
}
}
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<X509> newcert;
std::shared_ptr<EVP_PKEY> 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<EVP_PKEY>(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) */
try {
if (!VerifyCertificate(cacert, newcert, listener->GetCrlPath())) {
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;
}
} catch (const std::exception&) { } /* Swallow the exception on purpose, cacert will never be a non-CA certificate. */
/* 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.";
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()) {
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;
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 || listener->GetExternalCa())
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 << "'.";
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;
}