Skip to content

Commit 5acf3fb

Browse files
authored
Merge pull request #9934 from Icinga/renew-the-ca-9890-213
ApiListener#Start(): auto-renew CA on its owner
2 parents f483d37 + 7a8bd0f commit 5acf3fb

File tree

7 files changed

+218
-21
lines changed

7 files changed

+218
-21
lines changed

lib/base/tlsutility.cpp

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ String GetIcingaCADir()
706706
return Configuration::DataDir + "/ca";
707707
}
708708

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

@@ -743,7 +743,7 @@ std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject)
743743
EVP_PKEY *privkey = EVP_PKEY_new();
744744
EVP_PKEY_assign_RSA(privkey, rsa);
745745

746-
return CreateCert(pubkey, subject, X509_get_subject_name(cacert.get()), privkey, false);
746+
return CreateCert(pubkey, subject, X509_get_subject_name(cacert.get()), privkey, ca);
747747
}
748748

749749
std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert)
@@ -752,24 +752,37 @@ std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert)
752752
return CreateCertIcingaCA(pkey.get(), X509_get_subject_name(cert.get()));
753753
}
754754

755+
static inline
756+
bool CertExpiresWithin(X509* cert, int seconds)
757+
{
758+
time_t renewalStart = time(nullptr) + seconds;
759+
760+
return X509_cmp_time(X509_get_notAfter(cert), &renewalStart) < 0;
761+
}
762+
755763
bool IsCertUptodate(const std::shared_ptr<X509>& cert)
756764
{
757-
time_t now;
758-
time(&now);
765+
if (CertExpiresWithin(cert.get(), RENEW_THRESHOLD)) {
766+
return false;
767+
}
759768

760769
/* auto-renew all certificates which were created before 2017 to force an update of the CA,
761770
* because Icinga versions older than 2.4 sometimes create certificates with an invalid
762771
* serial number. */
763772
time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */
764-
time_t renewalStart = now + RENEW_THRESHOLD;
765773

766-
return X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1;
774+
return X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) >= 0;
775+
}
776+
777+
bool IsCaUptodate(X509* cert)
778+
{
779+
return !CertExpiresWithin(cert, LEAF_VALID_FOR);
767780
}
768781

769-
String CertificateToString(const std::shared_ptr<X509>& cert)
782+
String CertificateToString(X509* cert)
770783
{
771784
BIO *mem = BIO_new(BIO_s_mem());
772-
PEM_write_bio_X509(mem, cert.get());
785+
PEM_write_bio_X509(mem, cert);
773786

774787
char *data;
775788
long len = BIO_get_mem_data(mem, &data);

lib/base/tlsutility.hpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,18 @@ int MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile =
5555
std::shared_ptr<X509> CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca);
5656

5757
String GetIcingaCADir();
58-
String CertificateToString(const std::shared_ptr<X509>& cert);
58+
String CertificateToString(X509* cert);
59+
60+
inline String CertificateToString(const std::shared_ptr<X509>& cert)
61+
{
62+
return CertificateToString(cert.get());
63+
}
5964

6065
std::shared_ptr<X509> StringToCertificate(const String& cert);
61-
std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject);
66+
std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject, bool ca = false);
6267
std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert);
6368
bool IsCertUptodate(const std::shared_ptr<X509>& cert);
69+
bool IsCaUptodate(X509* cert);
6470

6571
String PBKDF2_SHA1(const String& password, const String& salt, int iterations);
6672
String PBKDF2_SHA256(const String& password, const String& salt, int iterations);

lib/remote/apilistener.cpp

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,12 @@ void ApiListener::OnConfigLoaded()
181181
UpdateSSLContext();
182182
}
183183

184-
std::shared_ptr<X509> ApiListener::RenewCert(const std::shared_ptr<X509>& cert)
184+
std::shared_ptr<X509> ApiListener::RenewCert(const std::shared_ptr<X509>& cert, bool ca)
185185
{
186186
std::shared_ptr<EVP_PKEY> pubkey (X509_get_pubkey(cert.get()), EVP_PKEY_free);
187187
auto subject (X509_get_subject_name(cert.get()));
188188
auto cacert (GetX509Certificate(GetDefaultCaPath()));
189-
auto newcert (CreateCertIcingaCA(pubkey.get(), subject));
189+
auto newcert (CreateCertIcingaCA(pubkey.get(), subject, ca));
190190

191191
/* verify that the new cert matches the CA we're using for the ApiListener;
192192
* this ensures that the CA we have in /var/lib/icinga2/ca matches the one
@@ -248,7 +248,12 @@ void ApiListener::Start(bool runtimeCreated)
248248

249249
if (Utility::PathExists(GetIcingaCADir() + "/ca.key")) {
250250
RenewOwnCert();
251-
m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) { RenewOwnCert(); });
251+
RenewCA();
252+
253+
m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) {
254+
RenewOwnCert();
255+
RenewCA();
256+
});
252257
} else {
253258
m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) {
254259
JsonRpcConnection::SendCertificateRequest(nullptr, nullptr, String());
@@ -329,6 +334,31 @@ void ApiListener::RenewOwnCert()
329334
UpdateSSLContext();
330335
}
331336

337+
void ApiListener::RenewCA()
338+
{
339+
auto certPath (GetCaDir() + "/ca.crt");
340+
auto cert (GetX509Certificate(certPath));
341+
342+
if (IsCaUptodate(cert.get())) {
343+
return;
344+
}
345+
346+
Log(LogInformation, "ApiListener")
347+
<< "Our CA will expire soon, but we own it. Renewing.";
348+
349+
cert = RenewCert(cert, true);
350+
351+
if (!cert) {
352+
return;
353+
}
354+
355+
auto certStr (CertificateToString(cert));
356+
357+
AtomicFile::Write(GetDefaultCaPath(), 0644, certStr);
358+
AtomicFile::Write(certPath, 0644, certStr);
359+
UpdateSSLContext();
360+
}
361+
332362
void ApiListener::Stop(bool runtimeDeleted)
333363
{
334364
ObjectImpl<ApiListener>::Stop(runtimeDeleted);

lib/remote/apilistener.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class ApiListener final : public ObjectImpl<ApiListener>
9090
static String GetCaDir();
9191
static String GetCertificateRequestsDir();
9292

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

9696
static ApiListener::Ptr GetInstance();
@@ -226,6 +226,7 @@ class ApiListener final : public ObjectImpl<ApiListener>
226226
void SyncLocalZoneDirs() const;
227227
void SyncLocalZoneDir(const Zone::Ptr& zone) const;
228228
void RenewOwnCert();
229+
void RenewCA();
229230

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

lib/remote/jsonrpcconnection-pki.cpp

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <boost/thread/once.hpp>
1515
#include <boost/regex.hpp>
1616
#include <fstream>
17+
#include <openssl/asn1.h>
1718
#include <openssl/ssl.h>
1819
#include <openssl/x509.h>
1920

@@ -31,11 +32,11 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
3132
std::shared_ptr<X509> cert;
3233

3334
Dictionary::Ptr result = new Dictionary();
35+
auto& tlsConn (origin->FromClient->GetStream()->next_layer());
3436

3537
/* Use the presented client certificate if not provided. */
3638
if (certText.IsEmpty()) {
37-
auto stream (origin->FromClient->GetStream());
38-
cert = stream->next_layer().GetPeerCertificate();
39+
cert = tlsConn.GetPeerCertificate();
3940
} else {
4041
cert = StringToCertificate(certText);
4142
}
@@ -76,13 +77,54 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
7677
}
7778
}
7879

80+
std::shared_ptr<X509> parsedRequestorCA;
81+
X509* requestorCA = nullptr;
82+
7983
if (signedByCA) {
80-
if (IsCertUptodate(cert)) {
84+
bool uptodate = IsCertUptodate(cert);
85+
86+
if (uptodate) {
87+
// Even if the leaf is up-to-date, the root may expire soon.
88+
// In a regular setup where Icinga manages the PKI, there is only one CA.
89+
// Icinga includes it in handshakes, let's see whether the peer needs a fresh one...
90+
91+
if (cn == origin->FromClient->GetIdentity()) {
92+
auto chain (SSL_get_peer_cert_chain(tlsConn.native_handle()));
93+
94+
if (chain) {
95+
auto len (sk_X509_num(chain));
8196

97+
for (int i = 0; i < len; ++i) {
98+
auto link (sk_X509_value(chain, i));
99+
100+
if (!X509_NAME_cmp(X509_get_subject_name(link), X509_get_issuer_name(link))) {
101+
requestorCA = link;
102+
}
103+
}
104+
}
105+
} else {
106+
Value requestorCaStr;
107+
108+
if (params->Get("requestor_ca", &requestorCaStr)) {
109+
parsedRequestorCA = StringToCertificate(requestorCaStr);
110+
requestorCA = parsedRequestorCA.get();
111+
}
112+
}
113+
114+
if (requestorCA && !IsCaUptodate(requestorCA)) {
115+
int days;
116+
117+
if (ASN1_TIME_diff(&days, nullptr, X509_get_notAfter(requestorCA), X509_get_notAfter(cacert.get())) && days > 0) {
118+
uptodate = false;
119+
}
120+
}
121+
}
122+
123+
if (uptodate) {
82124
Log(LogInformation, "JsonRpcConnection")
83-
<< "The certificate for CN '" << cn << "' is valid and uptodate. Skipping automated renewal.";
125+
<< "The certificates for CN '" << cn << "' and its root CA are valid and uptodate. Skipping automated renewal.";
84126
result->Set("status_code", 1);
85-
result->Set("error", "The certificate for CN '" + cn + "' is valid and uptodate. Skipping automated renewal.");
127+
result->Set("error", "The certificates for CN '" + cn + "' and its root CA are valid and uptodate. Skipping automated renewal.");
86128
return result;
87129
}
88130
}
@@ -229,6 +271,10 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
229271
{ "ticket", params->Get("ticket") }
230272
});
231273

274+
if (requestorCA) {
275+
request->Set("requestor_ca", CertificateToString(requestorCA));
276+
}
277+
232278
Utility::SaveJsonFile(requestPath, 0600, request);
233279

234280
JsonRpcConnection::SendCertificateRequest(nullptr, origin, requestPath);
@@ -290,8 +336,7 @@ void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& acl
290336
if (request->Contains("cert_response"))
291337
return;
292338

293-
params->Set("cert_request", request->Get("cert_request"));
294-
params->Set("ticket", request->Get("ticket"));
339+
request->CopyTo(params);
295340
}
296341

297342
/* Send the request to a) the connected client

test/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ add_boost_test(base
109109
base_timer/invoke
110110
base_timer/scope
111111
base_tlsutility/sha1
112+
base_tlsutility/iscauptodate_ok
113+
base_tlsutility/iscauptodate_expiring
114+
base_tlsutility/iscertuptodate_ok
115+
base_tlsutility/iscertuptodate_expiring
116+
base_tlsutility/iscertuptodate_old
112117
base_type/gettype
113118
base_type/assign
114119
base_type/byname

test/base-tlsutility.cpp

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,61 @@
22

33
#include "base/tlsutility.hpp"
44
#include <BoostTestTargetConfig.h>
5+
#include <functional>
6+
#include <memory>
7+
#include <openssl/asn1.h>
8+
#include <openssl/bn.h>
9+
#include <openssl/evp.h>
10+
#include <openssl/obj_mac.h>
11+
#include <openssl/rsa.h>
12+
#include <openssl/x509.h>
513
#include <utility>
614
#include <vector>
715

816
using namespace icinga;
917

18+
static EVP_PKEY* GenKeypair()
19+
{
20+
InitializeOpenSSL();
21+
22+
auto e (BN_new());
23+
BOOST_REQUIRE(e);
24+
25+
auto rsa (RSA_new());
26+
BOOST_REQUIRE(rsa);
27+
28+
auto key (EVP_PKEY_new());
29+
BOOST_REQUIRE(key);
30+
31+
BOOST_REQUIRE(BN_set_word(e, RSA_F4));
32+
BOOST_REQUIRE(RSA_generate_key_ex(rsa, 4096, e, nullptr));
33+
BOOST_REQUIRE(EVP_PKEY_assign_RSA(key, rsa));
34+
35+
return key;
36+
}
37+
38+
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)
39+
{
40+
auto cert (X509_new());
41+
BOOST_REQUIRE(cert);
42+
43+
auto serial (BN_new());
44+
BOOST_REQUIRE(serial);
45+
46+
BOOST_REQUIRE(X509_set_version(cert, 0x2));
47+
BOOST_REQUIRE(BN_to_ASN1_INTEGER(serial, X509_get_serialNumber(cert)));
48+
BOOST_REQUIRE(X509_NAME_add_entry_by_NID(X509_get_issuer_name(cert), NID_commonName, MBSTRING_ASC, (unsigned char*)issuer, -1, -1, 0));
49+
setTimes(X509_get_notBefore(cert), X509_get_notAfter(cert));
50+
BOOST_REQUIRE(X509_NAME_add_entry_by_NID(X509_get_subject_name(cert), NID_commonName, MBSTRING_ASC, (unsigned char*)subject, -1, -1, 0));
51+
BOOST_REQUIRE(X509_set_pubkey(cert, pubkey));
52+
BOOST_REQUIRE(X509_sign(cert, signer, EVP_sha256()));
53+
54+
return std::shared_ptr<X509>(cert, X509_free);
55+
}
56+
57+
static const long l_2016 = 1480000000; // Thu Nov 24 15:06:40 UTC 2016
58+
static const long l_2017 = 1490000000; // Mon Mar 20 08:53:20 UTC 2017
59+
1060
BOOST_AUTO_TEST_SUITE(base_tlsutility)
1161

1262
BOOST_AUTO_TEST_CASE(sha1)
@@ -35,4 +85,51 @@ BOOST_AUTO_TEST_CASE(sha1)
3585
}
3686
}
3787

88+
BOOST_AUTO_TEST_CASE(iscauptodate_ok)
89+
{
90+
auto key (GenKeypair());
91+
92+
BOOST_CHECK(IsCaUptodate(MakeCert("Icinga CA", key, "Icinga CA", key, [](ASN1_TIME* notBefore, ASN1_TIME* notAfter) {
93+
BOOST_REQUIRE(X509_gmtime_adj(notBefore, 0));
94+
BOOST_REQUIRE(X509_gmtime_adj(notAfter, LEAF_VALID_FOR + 60 * 60));
95+
}).get()));
96+
}
97+
98+
BOOST_AUTO_TEST_CASE(iscauptodate_expiring)
99+
{
100+
auto key (GenKeypair());
101+
102+
BOOST_CHECK(!IsCaUptodate(MakeCert("Icinga CA", key, "Icinga CA", key, [](ASN1_TIME* notBefore, ASN1_TIME* notAfter) {
103+
BOOST_REQUIRE(X509_gmtime_adj(notBefore, 0));
104+
BOOST_REQUIRE(X509_gmtime_adj(notAfter, LEAF_VALID_FOR - 60 * 60));
105+
}).get()));
106+
}
107+
108+
BOOST_AUTO_TEST_CASE(iscertuptodate_ok)
109+
{
110+
BOOST_CHECK(IsCertUptodate(MakeCert("Icinga CA", GenKeypair(), "example.com", GenKeypair(), [](ASN1_TIME* notBefore, ASN1_TIME* notAfter) {
111+
time_t epoch = 0;
112+
BOOST_REQUIRE(X509_time_adj(notBefore, l_2017, &epoch));
113+
BOOST_REQUIRE(X509_gmtime_adj(notAfter, RENEW_THRESHOLD + 60 * 60));
114+
})));
115+
}
116+
117+
BOOST_AUTO_TEST_CASE(iscertuptodate_expiring)
118+
{
119+
BOOST_CHECK(!IsCertUptodate(MakeCert("Icinga CA", GenKeypair(), "example.com", GenKeypair(), [](ASN1_TIME* notBefore, ASN1_TIME* notAfter) {
120+
time_t epoch = 0;
121+
BOOST_REQUIRE(X509_time_adj(notBefore, l_2017, &epoch));
122+
BOOST_REQUIRE(X509_gmtime_adj(notAfter, RENEW_THRESHOLD - 60 * 60));
123+
})));
124+
}
125+
126+
BOOST_AUTO_TEST_CASE(iscertuptodate_old)
127+
{
128+
BOOST_CHECK(!IsCertUptodate(MakeCert("Icinga CA", GenKeypair(), "example.com", GenKeypair(), [](ASN1_TIME* notBefore, ASN1_TIME* notAfter) {
129+
time_t epoch = 0;
130+
BOOST_REQUIRE(X509_time_adj(notBefore, l_2016, &epoch));
131+
BOOST_REQUIRE(X509_gmtime_adj(notAfter, RENEW_THRESHOLD + 60 * 60));
132+
})));
133+
}
134+
38135
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)