diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index dc2e7ba73..2519b49cc 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) @@ -760,24 +760,37 @@ 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; } -String CertificateToString(const std::shared_ptr& cert) +bool IsCaUptodate(X509* cert) +{ + return !CertExpiresWithin(cert, LEAF_VALID_FOR); +} + +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 968e55a19..b06412020 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -58,12 +58,18 @@ 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); +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); String PBKDF2_SHA1(const String& password, const String& salt, int iterations); String PBKDF2_SHA256(const String& password, const String& salt, int iterations); diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp index f5e6e302a..85443e218 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 @@ -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 ffe97a2b3..fced0a8af 100644 --- a/lib/remote/apilistener.hpp +++ b/lib/remote/apilistener.hpp @@ -91,7 +91,7 @@ public: 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(); @@ -227,6 +227,7 @@ private: void SyncLocalZoneDirs() const; void SyncLocalZoneDir(const Zone::Ptr& zone) const; void RenewOwnCert(); + void RenewCA(); void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient); diff --git a/lib/remote/jsonrpcconnection-pki.cpp b/lib/remote/jsonrpcconnection-pki.cpp index d2b727b67..340e12b30 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 } } - if (signedByCA) { - if (IsCertUptodate(cert)) { + std::shared_ptr parsedRequestorCA; + X509* requestorCA = nullptr; + if (signedByCA) { + 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 @@ delayed_request: { "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 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 24eb2198c..cd6c9b8b6 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 c66cef474..2e611e49a 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()