From 0f4723e5670041e8121b5f52034659ce39454d9f Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Tue, 12 Dec 2023 17:07:42 +0100 Subject: [PATCH 1/7] Introduce IsCaUptodate() by splitting IsCertUptodate() --- lib/base/tlsutility.cpp | 21 +++++++++++++++++---- lib/base/tlsutility.hpp | 1 + 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index dc2e7ba73fa..5912596d160 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -760,18 +760,31 @@ std::shared_ptr CreateCertIcingaCA(const std::shared_ptr& cert) return CreateCertIcingaCA(pkey.get(), X509_get_subject_name(cert.get())); } +static inline +bool CertExpiresWithin(X509* cert, int seconds) +{ + time_t renewalStart = time(nullptr) + seconds; + + return X509_cmp_time(X509_get_notAfter(cert), &renewalStart) < 0; +} + bool IsCertUptodate(const std::shared_ptr& cert) { - time_t now; - time(&now); + if (CertExpiresWithin(cert.get(), RENEW_THRESHOLD)) { + return false; + } /* 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 + RENEW_THRESHOLD; - return X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1; + return X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) >= 0; +} + +bool IsCaUptodate(X509* cert) +{ + return !CertExpiresWithin(cert, LEAF_VALID_FOR); } String CertificateToString(const std::shared_ptr& cert) diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp index 968e55a19a0..523b30d5d45 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -64,6 +64,7 @@ std::shared_ptr StringToCertificate(const String& cert); std::shared_ptr CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject); std::shared_ptr CreateCertIcingaCA(const std::shared_ptr& cert); bool IsCertUptodate(const std::shared_ptr& cert); +bool IsCaUptodate(X509* cert); String PBKDF2_SHA1(const String& password, const String& salt, int iterations); String PBKDF2_SHA256(const String& password, const String& salt, int iterations); From 17eac308682bdd1752df619d0b88d48c55a25c6d Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Tue, 12 Dec 2023 17:15:25 +0100 Subject: [PATCH 2/7] Test IsCertUptodate() and IsCaUptodate() --- test/CMakeLists.txt | 5 +++ test/base-tlsutility.cpp | 97 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8919de304dc..753e1776df1 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -109,6 +109,11 @@ add_boost_test(base base_timer/invoke base_timer/scope base_tlsutility/sha1 + base_tlsutility/iscauptodate_ok + base_tlsutility/iscauptodate_expiring + base_tlsutility/iscertuptodate_ok + base_tlsutility/iscertuptodate_expiring + base_tlsutility/iscertuptodate_old base_type/gettype base_type/assign base_type/byname diff --git a/test/base-tlsutility.cpp b/test/base-tlsutility.cpp index c66cef474ef..2e611e49ae8 100644 --- a/test/base-tlsutility.cpp +++ b/test/base-tlsutility.cpp @@ -2,11 +2,61 @@ #include "base/tlsutility.hpp" #include +#include +#include +#include +#include +#include +#include +#include +#include #include #include using namespace icinga; +static EVP_PKEY* GenKeypair() +{ + InitializeOpenSSL(); + + auto e (BN_new()); + BOOST_REQUIRE(e); + + auto rsa (RSA_new()); + BOOST_REQUIRE(rsa); + + auto key (EVP_PKEY_new()); + BOOST_REQUIRE(key); + + BOOST_REQUIRE(BN_set_word(e, RSA_F4)); + BOOST_REQUIRE(RSA_generate_key_ex(rsa, 4096, e, nullptr)); + BOOST_REQUIRE(EVP_PKEY_assign_RSA(key, rsa)); + + return key; +} + +static std::shared_ptr MakeCert(const char* issuer, EVP_PKEY* signer, const char* subject, EVP_PKEY* pubkey, std::function setTimes) +{ + auto cert (X509_new()); + BOOST_REQUIRE(cert); + + auto serial (BN_new()); + BOOST_REQUIRE(serial); + + BOOST_REQUIRE(X509_set_version(cert, 0x2)); + BOOST_REQUIRE(BN_to_ASN1_INTEGER(serial, X509_get_serialNumber(cert))); + BOOST_REQUIRE(X509_NAME_add_entry_by_NID(X509_get_issuer_name(cert), NID_commonName, MBSTRING_ASC, (unsigned char*)issuer, -1, -1, 0)); + setTimes(X509_get_notBefore(cert), X509_get_notAfter(cert)); + BOOST_REQUIRE(X509_NAME_add_entry_by_NID(X509_get_subject_name(cert), NID_commonName, MBSTRING_ASC, (unsigned char*)subject, -1, -1, 0)); + BOOST_REQUIRE(X509_set_pubkey(cert, pubkey)); + BOOST_REQUIRE(X509_sign(cert, signer, EVP_sha256())); + + return std::shared_ptr(cert, X509_free); +} + +static const long l_2016 = 1480000000; // Thu Nov 24 15:06:40 UTC 2016 +static const long l_2017 = 1490000000; // Mon Mar 20 08:53:20 UTC 2017 + BOOST_AUTO_TEST_SUITE(base_tlsutility) BOOST_AUTO_TEST_CASE(sha1) @@ -35,4 +85,51 @@ BOOST_AUTO_TEST_CASE(sha1) } } +BOOST_AUTO_TEST_CASE(iscauptodate_ok) +{ + auto key (GenKeypair()); + + BOOST_CHECK(IsCaUptodate(MakeCert("Icinga CA", key, "Icinga CA", key, [](ASN1_TIME* notBefore, ASN1_TIME* notAfter) { + BOOST_REQUIRE(X509_gmtime_adj(notBefore, 0)); + BOOST_REQUIRE(X509_gmtime_adj(notAfter, LEAF_VALID_FOR + 60 * 60)); + }).get())); +} + +BOOST_AUTO_TEST_CASE(iscauptodate_expiring) +{ + auto key (GenKeypair()); + + BOOST_CHECK(!IsCaUptodate(MakeCert("Icinga CA", key, "Icinga CA", key, [](ASN1_TIME* notBefore, ASN1_TIME* notAfter) { + BOOST_REQUIRE(X509_gmtime_adj(notBefore, 0)); + BOOST_REQUIRE(X509_gmtime_adj(notAfter, LEAF_VALID_FOR - 60 * 60)); + }).get())); +} + +BOOST_AUTO_TEST_CASE(iscertuptodate_ok) +{ + BOOST_CHECK(IsCertUptodate(MakeCert("Icinga CA", GenKeypair(), "example.com", GenKeypair(), [](ASN1_TIME* notBefore, ASN1_TIME* notAfter) { + time_t epoch = 0; + BOOST_REQUIRE(X509_time_adj(notBefore, l_2017, &epoch)); + BOOST_REQUIRE(X509_gmtime_adj(notAfter, RENEW_THRESHOLD + 60 * 60)); + }))); +} + +BOOST_AUTO_TEST_CASE(iscertuptodate_expiring) +{ + BOOST_CHECK(!IsCertUptodate(MakeCert("Icinga CA", GenKeypair(), "example.com", GenKeypair(), [](ASN1_TIME* notBefore, ASN1_TIME* notAfter) { + time_t epoch = 0; + BOOST_REQUIRE(X509_time_adj(notBefore, l_2017, &epoch)); + BOOST_REQUIRE(X509_gmtime_adj(notAfter, RENEW_THRESHOLD - 60 * 60)); + }))); +} + +BOOST_AUTO_TEST_CASE(iscertuptodate_old) +{ + BOOST_CHECK(!IsCertUptodate(MakeCert("Icinga CA", GenKeypair(), "example.com", GenKeypair(), [](ASN1_TIME* notBefore, ASN1_TIME* notAfter) { + time_t epoch = 0; + BOOST_REQUIRE(X509_time_adj(notBefore, l_2016, &epoch)); + BOOST_REQUIRE(X509_gmtime_adj(notAfter, RENEW_THRESHOLD + 60 * 60)); + }))); +} + BOOST_AUTO_TEST_SUITE_END() From 0cb037c69854a7895bab412020ab7d09b5246524 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Mon, 6 Nov 2023 10:28:55 +0100 Subject: [PATCH 3/7] CreateCertIcingaCA(EVP_PKEY*, X509_NAME*): enable optional CA creation --- lib/base/tlsutility.cpp | 4 ++-- lib/base/tlsutility.hpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index 5912596d160..ae1c163a1a2 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -714,7 +714,7 @@ String GetIcingaCADir() return Configuration::DataDir + "/ca"; } -std::shared_ptr CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject) +std::shared_ptr CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject, bool ca) { char errbuf[256]; @@ -751,7 +751,7 @@ std::shared_ptr CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject) EVP_PKEY *privkey = EVP_PKEY_new(); EVP_PKEY_assign_RSA(privkey, rsa); - return CreateCert(pubkey, subject, X509_get_subject_name(cacert.get()), privkey, false); + return CreateCert(pubkey, subject, X509_get_subject_name(cacert.get()), privkey, ca); } std::shared_ptr CreateCertIcingaCA(const std::shared_ptr& cert) diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp index 523b30d5d45..3cfb8316ca8 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -61,7 +61,7 @@ String GetIcingaCADir(); String CertificateToString(const std::shared_ptr& cert); std::shared_ptr StringToCertificate(const String& cert); -std::shared_ptr CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject); +std::shared_ptr CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject, bool ca = false); std::shared_ptr CreateCertIcingaCA(const std::shared_ptr& cert); bool IsCertUptodate(const std::shared_ptr& cert); bool IsCaUptodate(X509* cert); From b3dee0bb0a94bc2063e956db8326a8e77dc8b848 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Mon, 6 Nov 2023 10:34:16 +0100 Subject: [PATCH 4/7] ApiListener#RenewCert(): enable optional CA creation --- lib/remote/apilistener.cpp | 4 ++-- lib/remote/apilistener.hpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp index f5e6e302a8b..f0ac42cc45c 100644 --- a/lib/remote/apilistener.cpp +++ b/lib/remote/apilistener.cpp @@ -181,12 +181,12 @@ void ApiListener::OnConfigLoaded() UpdateSSLContext(); } -std::shared_ptr ApiListener::RenewCert(const std::shared_ptr& cert) +std::shared_ptr ApiListener::RenewCert(const std::shared_ptr& cert, bool ca) { std::shared_ptr pubkey (X509_get_pubkey(cert.get()), EVP_PKEY_free); auto subject (X509_get_subject_name(cert.get())); auto cacert (GetX509Certificate(GetDefaultCaPath())); - auto newcert (CreateCertIcingaCA(pubkey.get(), subject)); + auto newcert (CreateCertIcingaCA(pubkey.get(), subject, ca)); /* 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 diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp index ffe97a2b324..48e7e4c427f 100644 --- a/lib/remote/apilistener.hpp +++ b/lib/remote/apilistener.hpp @@ -91,7 +91,7 @@ class ApiListener final : public ObjectImpl static String GetCaDir(); static String GetCertificateRequestsDir(); - std::shared_ptr RenewCert(const std::shared_ptr& cert); + std::shared_ptr RenewCert(const std::shared_ptr& cert, bool ca = false); void UpdateSSLContext(); static ApiListener::Ptr GetInstance(); From 32f43c4873a18bc8ef4b50228634e9f49a8623a7 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Fri, 27 Oct 2023 18:24:29 +0200 Subject: [PATCH 5/7] ApiListener#Start(): auto-renew CA on its owner otherwise it would expire. --- lib/remote/apilistener.cpp | 32 +++++++++++++++++++++++++++++++- lib/remote/apilistener.hpp | 1 + 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp index f0ac42cc45c..85443e21850 100644 --- a/lib/remote/apilistener.cpp +++ b/lib/remote/apilistener.cpp @@ -248,7 +248,12 @@ void ApiListener::Start(bool runtimeCreated) if (Utility::PathExists(GetIcingaCADir() + "/ca.key")) { RenewOwnCert(); - m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) { RenewOwnCert(); }); + RenewCA(); + + m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) { + RenewOwnCert(); + RenewCA(); + }); } else { m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) { JsonRpcConnection::SendCertificateRequest(nullptr, nullptr, String()); @@ -329,6 +334,31 @@ void ApiListener::RenewOwnCert() UpdateSSLContext(); } +void ApiListener::RenewCA() +{ + auto certPath (GetCaDir() + "/ca.crt"); + auto cert (GetX509Certificate(certPath)); + + if (IsCaUptodate(cert.get())) { + return; + } + + Log(LogInformation, "ApiListener") + << "Our CA will expire soon, but we own it. Renewing."; + + cert = RenewCert(cert, true); + + if (!cert) { + return; + } + + auto certStr (CertificateToString(cert)); + + AtomicFile::Write(GetDefaultCaPath(), 0644, certStr); + AtomicFile::Write(certPath, 0644, certStr); + UpdateSSLContext(); +} + void ApiListener::Stop(bool runtimeDeleted) { m_ApiPackageIntegrityTimer->Stop(true); diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp index 48e7e4c427f..fced0a8afb1 100644 --- a/lib/remote/apilistener.hpp +++ b/lib/remote/apilistener.hpp @@ -227,6 +227,7 @@ class ApiListener final : public ObjectImpl void SyncLocalZoneDirs() const; void SyncLocalZoneDir(const Zone::Ptr& zone) const; void RenewOwnCert(); + void RenewCA(); void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient); From 6b000fbce6558eb1cfdbd19ccabe58dc2833f5c3 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Fri, 8 Dec 2023 10:35:14 +0100 Subject: [PATCH 6/7] CertificateToString(): allow raw pointer input --- lib/base/tlsutility.cpp | 4 ++-- lib/base/tlsutility.hpp | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index ae1c163a1a2..2519b49cca9 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -787,10 +787,10 @@ bool IsCaUptodate(X509* cert) return !CertExpiresWithin(cert, LEAF_VALID_FOR); } -String CertificateToString(const std::shared_ptr& cert) +String CertificateToString(X509* cert) { BIO *mem = BIO_new(BIO_s_mem()); - PEM_write_bio_X509(mem, cert.get()); + PEM_write_bio_X509(mem, cert); char *data; long len = BIO_get_mem_data(mem, &data); diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp index 3cfb8316ca8..b0641202011 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -58,7 +58,12 @@ int MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile = std::shared_ptr CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca); String GetIcingaCADir(); -String CertificateToString(const std::shared_ptr& cert); +String CertificateToString(X509* cert); + +inline String CertificateToString(const std::shared_ptr& cert) +{ + return CertificateToString(cert.get()); +} std::shared_ptr StringToCertificate(const String& cert); std::shared_ptr CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject, bool ca = false); From 4195f8d0f0a0c907261c549f4b9509079db25e09 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Thu, 9 Nov 2023 10:33:22 +0100 Subject: [PATCH 7/7] RequestCertificateHandler(): also renew if CA needs a renewal and a newer one is available. --- lib/remote/jsonrpcconnection-pki.cpp | 59 ++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/lib/remote/jsonrpcconnection-pki.cpp b/lib/remote/jsonrpcconnection-pki.cpp index d2b727b674f..340e12b301e 100644 --- a/lib/remote/jsonrpcconnection-pki.cpp +++ b/lib/remote/jsonrpcconnection-pki.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -31,11 +32,11 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona std::shared_ptr cert; Dictionary::Ptr result = new Dictionary(); + auto& tlsConn (origin->FromClient->GetStream()->next_layer()); /* Use the presented client certificate if not provided. */ if (certText.IsEmpty()) { - auto stream (origin->FromClient->GetStream()); - cert = stream->next_layer().GetPeerCertificate(); + cert = tlsConn.GetPeerCertificate(); } else { cert = StringToCertificate(certText); } @@ -77,13 +78,54 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona } } + std::shared_ptr parsedRequestorCA; + X509* requestorCA = nullptr; + if (signedByCA) { - if (IsCertUptodate(cert)) { + bool uptodate = IsCertUptodate(cert); + + if (uptodate) { + // Even if the leaf is up-to-date, the root may expire soon. + // In a regular setup where Icinga manages the PKI, there is only one CA. + // Icinga includes it in handshakes, let's see whether the peer needs a fresh one... + + if (cn == origin->FromClient->GetIdentity()) { + auto chain (SSL_get_peer_cert_chain(tlsConn.native_handle())); + + if (chain) { + auto len (sk_X509_num(chain)); + for (int i = 0; i < len; ++i) { + auto link (sk_X509_value(chain, i)); + + if (!X509_NAME_cmp(X509_get_subject_name(link), X509_get_issuer_name(link))) { + requestorCA = link; + } + } + } + } else { + Value requestorCaStr; + + if (params->Get("requestor_ca", &requestorCaStr)) { + parsedRequestorCA = StringToCertificate(requestorCaStr); + requestorCA = parsedRequestorCA.get(); + } + } + + if (requestorCA && !IsCaUptodate(requestorCA)) { + int days; + + if (ASN1_TIME_diff(&days, nullptr, X509_get_notAfter(requestorCA), X509_get_notAfter(cacert.get())) && days > 0) { + uptodate = false; + } + } + } + + if (uptodate) { Log(LogInformation, "JsonRpcConnection") - << "The certificate for CN '" << cn << "' is valid and uptodate. Skipping automated renewal."; + << "The certificates for CN '" << cn << "' and its root CA are 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."); + result->Set("error", "The certificates for CN '" + cn + "' and its root CA are valid and uptodate. Skipping automated renewal."); return result; } } @@ -230,6 +272,10 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona { "ticket", params->Get("ticket") } }); + if (requestorCA) { + request->Set("requestor_ca", CertificateToString(requestorCA)); + } + Utility::SaveJsonFile(requestPath, 0600, request); JsonRpcConnection::SendCertificateRequest(nullptr, origin, requestPath); @@ -291,8 +337,7 @@ void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& acl if (request->Contains("cert_response")) return; - params->Set("cert_request", request->Get("cert_request")); - params->Set("ticket", request->Get("ticket")); + request->CopyTo(params); } /* Send the request to a) the connected client