Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ApiListener#Start(): auto-renew CA on its owner #9933

Merged
merged 7 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions lib/base/tlsutility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ String GetIcingaCADir()
return Configuration::DataDir + "/ca";
}

std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject)
std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject, bool ca)
{
char errbuf[256];

Expand Down Expand Up @@ -751,7 +751,7 @@ std::shared_ptr<X509> 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<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert)
Expand All @@ -760,24 +760,37 @@ std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& 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<X509>& 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<X509>& 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);
Expand Down
10 changes: 8 additions & 2 deletions lib/base/tlsutility.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,18 @@ int MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile =
std::shared_ptr<X509> CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca);

String GetIcingaCADir();
String CertificateToString(const std::shared_ptr<X509>& cert);
String CertificateToString(X509* cert);

inline String CertificateToString(const std::shared_ptr<X509>& cert)
{
return CertificateToString(cert.get());
}

std::shared_ptr<X509> StringToCertificate(const String& cert);
std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject);
std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject, bool ca = false);
std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert);
bool IsCertUptodate(const std::shared_ptr<X509>& 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);
Expand Down
36 changes: 33 additions & 3 deletions lib/remote/apilistener.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,12 @@ void ApiListener::OnConfigLoaded()
UpdateSSLContext();
}

std::shared_ptr<X509> ApiListener::RenewCert(const std::shared_ptr<X509>& cert)
std::shared_ptr<X509> ApiListener::RenewCert(const std::shared_ptr<X509>& cert, bool ca)
{
std::shared_ptr<EVP_PKEY> 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
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion lib/remote/apilistener.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class ApiListener final : public ObjectImpl<ApiListener>
static String GetCaDir();
static String GetCertificateRequestsDir();

std::shared_ptr<X509> RenewCert(const std::shared_ptr<X509>& cert);
std::shared_ptr<X509> RenewCert(const std::shared_ptr<X509>& cert, bool ca = false);
void UpdateSSLContext();

static ApiListener::Ptr GetInstance();
Expand Down Expand Up @@ -227,6 +227,7 @@ class ApiListener final : public ObjectImpl<ApiListener>
void SyncLocalZoneDirs() const;
void SyncLocalZoneDir(const Zone::Ptr& zone) const;
void RenewOwnCert();
void RenewCA();

void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient);

Expand Down
59 changes: 52 additions & 7 deletions lib/remote/jsonrpcconnection-pki.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <boost/thread/once.hpp>
#include <boost/regex.hpp>
#include <fstream>
#include <openssl/asn1.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>

Expand All @@ -31,11 +32,11 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
std::shared_ptr<X509> 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);
}
Expand Down Expand Up @@ -77,13 +78,54 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
}
}

std::shared_ptr<X509> 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;
}
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
97 changes: 97 additions & 0 deletions test/base-tlsutility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,61 @@

#include "base/tlsutility.hpp"
#include <BoostTestTargetConfig.h>
#include <functional>
#include <memory>
#include <openssl/asn1.h>
#include <openssl/bn.h>
#include <openssl/evp.h>
#include <openssl/obj_mac.h>
#include <openssl/rsa.h>
#include <openssl/x509.h>
#include <utility>
#include <vector>

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<X509> MakeCert(const char* issuer, EVP_PKEY* signer, const char* subject, EVP_PKEY* pubkey, std::function<void(ASN1_TIME*, ASN1_TIME*)> 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<X509>(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)
Expand Down Expand Up @@ -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()
Loading