Skip to content

Commit 79c98f8

Browse files
committed
Test internal cert generation & verification process
1 parent f308bb1 commit 79c98f8

File tree

2 files changed

+164
-79
lines changed

2 files changed

+164
-79
lines changed

test/CMakeLists.txt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,8 @@ add_boost_test(base
170170
base_timer/invoke
171171
base_timer/scope
172172
base_tlsutility/sha1
173-
base_tlsutility/iscauptodate_ok
174-
base_tlsutility/iscauptodate_expiring
175-
base_tlsutility/iscertuptodate_ok
176-
base_tlsutility/iscertuptodate_expiring
177-
base_tlsutility/iscertuptodate_old
173+
base_tlsutility/create_verify_ca
174+
base_tlsutility/create_verify_leaf_certs
178175
base_utility/parse_version
179176
base_utility/compare_version
180177
base_utility/comparepasswords_works

test/base-tlsutility.cpp

Lines changed: 162 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,40 @@
11
/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
22

3+
#include "base/configuration.hpp"
34
#include "base/tlsutility.hpp"
5+
#include "base/utility.hpp"
6+
#include "remote/apilistener.hpp"
7+
#include "remote/pkiutility.hpp"
48
#include <BoostTestTargetConfig.h>
9+
#include <boost/filesystem/operations.hpp>
10+
#include <boost/filesystem/path.hpp>
511
#include <functional>
612
#include <memory>
7-
#include <openssl/asn1.h>
8-
#include <openssl/bn.h>
913
#include <openssl/evp.h>
10-
#include <openssl/obj_mac.h>
11-
#include <openssl/rsa.h>
1214
#include <openssl/x509.h>
1315
#include <utility>
1416
#include <vector>
1517

1618
using namespace icinga;
1719

18-
static EVP_PKEY* GenKeypair()
20+
struct ConfigurationConstantsFixture
1921
{
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()));
22+
String TmpDir;
23+
String PreviousDataDir;
24+
25+
ConfigurationConstantsFixture()
26+
{
27+
TmpDir = boost::filesystem::detail::temp_directory_path().string() + "/icinga2";
28+
PreviousDataDir = Configuration::DataDir;
29+
Configuration::DataDir = TmpDir;
30+
}
5331

54-
return std::shared_ptr<X509>(cert, X509_free);
55-
}
32+
~ConfigurationConstantsFixture()
33+
{
34+
Configuration::DataDir = PreviousDataDir;
35+
Utility::RemoveDirRecursive(TmpDir);
36+
}
37+
};
5638

5739
static const long l_2016 = 1480000000; // Thu Nov 24 15:06:40 UTC 2016
5840
static const long l_2017 = 1490000000; // Mon Mar 20 08:53:20 UTC 2017
@@ -85,51 +67,157 @@ BOOST_AUTO_TEST_CASE(sha1)
8567
}
8668
}
8769

88-
BOOST_AUTO_TEST_CASE(iscauptodate_ok)
70+
static std::shared_ptr<EVP_PKEY> GetEVP_PKEY(const String& keyfile)
8971
{
90-
auto key (GenKeypair());
72+
BIO *cakeybio = BIO_new_file(keyfile.CStr(), "r");
73+
BOOST_REQUIRE_MESSAGE(cakeybio, "BIO_new_file() for private key from'" << keyfile << "': " << ERR_error_string(ERR_get_error(), nullptr));
9174

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-
}
75+
RSA* rsa = PEM_read_bio_RSAPrivateKey(cakeybio, nullptr, nullptr, nullptr);
76+
BOOST_REQUIRE_MESSAGE(rsa, "PEM read bio RSA key from private key file '" << keyfile << "': " << ERR_error_string(ERR_get_error(), nullptr));
9777

98-
BOOST_AUTO_TEST_CASE(iscauptodate_expiring)
99-
{
100-
auto key (GenKeypair());
78+
BIO_free(cakeybio);
79+
BOOST_CHECK_EQUAL(1, RSA_check_key(rsa)); // 1 == valid, 0 == invalid
10180

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-
}
81+
EVP_PKEY *pkey = EVP_PKEY_new();
82+
EVP_PKEY_assign_RSA(pkey, rsa);
83+
BOOST_CHECK_EQUAL(EVP_PKEY_RSA, EVP_PKEY_id(pkey));
10784

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-
})));
85+
return std::shared_ptr<EVP_PKEY>(pkey, EVP_PKEY_free);
11586
}
11687

117-
BOOST_AUTO_TEST_CASE(iscertuptodate_expiring)
88+
BOOST_AUTO_TEST_CASE(create_verify_ca)
11889
{
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-
})));
90+
ConfigurationConstantsFixture f;
91+
92+
BOOST_CHECK_EQUAL(0, PkiUtility::NewCa());
93+
94+
auto cacert(GetX509Certificate(ApiListener::GetCaDir()+"/ca.crt"));
95+
if (OPENSSL_VERSION_NUMBER >= 0x10100000L) {
96+
// OpenSSL 1.1.x provides https://www.openssl.org/docs/man1.1.0/man3/X509_check_ca.html
97+
BOOST_CHECK_EQUAL(true, IsCa(cacert));
98+
} else {
99+
BOOST_CHECK_THROW(IsCa(cacert), std::invalid_argument);
100+
}
101+
BOOST_CHECK_EQUAL(true, VerifyCertificate(cacert, cacert, String()));
102+
BOOST_CHECK_EQUAL(true, IsCaUptodate(cacert.get()));
103+
104+
time_t caValidUntil(time(nullptr) + ROOT_VALID_FOR);
105+
BOOST_CHECK_EQUAL(-1, X509_cmp_time(X509_get_notAfter(cacert.get()), &caValidUntil));
106+
107+
// Set the CA certificate to expire in 100 days, i.e. less than the LEAF_VALID_FOR threshold of 397 days.
108+
BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cacert.get()), 60*60*24*100));
109+
BOOST_CHECK_EQUAL(false, IsCaUptodate(cacert.get()));
110+
111+
// Even if the CA is going to expire at exactly the same time as the LEAF_VALID_FOR threshold,
112+
// it is still considered to be outdated.
113+
BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cacert.get()), 60*60*24*397));
114+
BOOST_CHECK_EQUAL(false, IsCaUptodate(cacert.get()));
115+
116+
// Reset the CA expiration date to the original value, i.e. 15 years.
117+
BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cacert.get()), ROOT_VALID_FOR));
118+
BOOST_CHECK_EQUAL(true, IsCaUptodate(cacert.get()));
124119
}
125120

126-
BOOST_AUTO_TEST_CASE(iscertuptodate_old)
121+
BOOST_AUTO_TEST_CASE(create_verify_leaf_certs)
127122
{
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-
})));
123+
ConfigurationConstantsFixture f;
124+
125+
String caDir = ApiListener::GetCaDir();
126+
String certsDir = ApiListener::GetCertsDir();
127+
Utility::MkDirP(certsDir, 0700);
128+
129+
BOOST_CHECK_EQUAL(0, PkiUtility::NewCa());
130+
131+
auto caprivatekey(GetEVP_PKEY(caDir+"/ca.key"));
132+
auto cacert(GetX509Certificate(caDir+"/ca.crt"));
133+
BOOST_CHECK_EQUAL(true, IsCaUptodate(cacert.get()));
134+
BOOST_CHECK_EQUAL(1, X509_verify(cacert.get(), caprivatekey.get())); // 1 == equal, 0 == unequal, -1 == error
135+
136+
BOOST_CHECK_EQUAL(0, PkiUtility::NewCert("example.com", certsDir+"example.key", certsDir+"example.csr", certsDir+"example.crt"));
137+
BOOST_CHECK_EQUAL(0, PkiUtility::SignCsr(certsDir+"example.csr", certsDir+"example.crt"));
138+
139+
auto cert(GetX509Certificate(certsDir+"/example.crt"));
140+
if (OPENSSL_VERSION_NUMBER >= 0x10100000L) {
141+
BOOST_CHECK_EQUAL(false, IsCa(cert));
142+
} else {
143+
BOOST_CHECK_THROW(IsCa(cert), std::invalid_argument);
144+
}
145+
BOOST_CHECK_EQUAL(true, IsCertUptodate(cert));
146+
BOOST_CHECK_EQUAL(true, VerifyCertificate(cacert, cert, String()));
147+
148+
time_t certValidUntil(time(nullptr) + LEAF_VALID_FOR);
149+
BOOST_CHECK_EQUAL(-1, X509_cmp_time(X509_get_notAfter(cert.get()), &certValidUntil));
150+
151+
// Set the certificate to expire in 20 days, i.e. less than the RENEW_THRESHOLD of 30 days.
152+
BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cert.get()), 60*60*24*20));
153+
BOOST_CHECK_EQUAL(false, IsCertUptodate(cert));
154+
155+
// Check whether expired certificates are correctly detected and verification fails.
156+
BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cert.get()), -10));
157+
BOOST_CHECK_EQUAL(false, IsCertUptodate(cert));
158+
BOOST_CHECK_THROW(VerifyCertificate(cacert, cert, String()), openssl_error);
159+
160+
// Reset the certificate expiration date to the original value, i.e. 397 days.
161+
BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cert.get()), LEAF_VALID_FOR));
162+
BOOST_CHECK_EQUAL(true, IsCertUptodate(cert));
163+
BOOST_CHECK_EQUAL(true, VerifyCertificate(cacert, cert, String()));
164+
165+
// Set the certificate validity start date to 2016, all certificates created before 2017 are considered outdated.
166+
BOOST_CHECK(X509_gmtime_adj(X509_get_notBefore(cert.get()), -(time(nullptr)-l_2016)));
167+
BOOST_CHECK_EQUAL(false, IsCertUptodate(cert));
168+
BOOST_CHECK_EQUAL(true, VerifyCertificate(cacert, cert, String()));
169+
170+
// Reset the certificate validity start date to the least acceptable value, i.e. 2017.
171+
BOOST_CHECK(X509_gmtime_adj(X509_get_notBefore(cert.get()), -(time(nullptr)-l_2017)));
172+
BOOST_CHECK_EQUAL(true, IsCertUptodate(cert));
173+
BOOST_CHECK_EQUAL(true, VerifyCertificate(cacert, cert, String()));
174+
175+
// Even if the leaf is up-to-date, the root CA has expired 10 days ago, so verification should fail.
176+
BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cacert.get()), -10));
177+
BOOST_CHECK_EQUAL(false, IsCaUptodate(cacert.get()));
178+
BOOST_CHECK_THROW(VerifyCertificate(cacert, cert, String()), openssl_error);
179+
180+
// Generate a new CA certificate to simulate a renewal and check whether verification still works.
181+
std::shared_ptr<EVP_PKEY> caPubKey(X509_get_pubkey(cacert.get()), EVP_PKEY_free);
182+
auto subject(X509_get_subject_name(cacert.get()));
183+
auto newCACert(CreateCertIcingaCA(caPubKey.get(), subject, true));
184+
BOOST_REQUIRE(newCACert);
185+
BOOST_CHECK_EQUAL(1, X509_verify(newCACert.get(), caprivatekey.get())); // 1 == equal, 0 == unequal, -1 == error
186+
BOOST_CHECK_EQUAL(true, IsCaUptodate(newCACert.get()));
187+
BOOST_CHECK_EQUAL(true, VerifyCertificate(newCACert, newCACert, String()));
188+
BOOST_CHECK_EQUAL(true, VerifyCertificate(newCACert, cert, String()));
189+
BOOST_CHECK_THROW(VerifyCertificate(cacert, newCACert, String()), openssl_error);
190+
191+
// Remove the previously generated CA before regenerating a new one, PkiUtility::NewCa() would fail otherwise.
192+
Utility::RemoveDirRecursive(caDir);
193+
BOOST_CHECK_EQUAL(0, PkiUtility::NewCa());
194+
195+
newCACert.reset(); // Reset the shared pointer to the old CA certificate, just to be sure.
196+
197+
newCACert = GetX509Certificate(caDir+"/ca.crt");
198+
auto newCAPrivateKey = GetEVP_PKEY(caDir+"/ca.key");
199+
BOOST_REQUIRE(newCACert);
200+
BOOST_CHECK_NE(0, ASN1_INTEGER_cmp(X509_get_serialNumber(cacert.get()), X509_get_serialNumber(newCACert.get())));
201+
BOOST_CHECK_NE(1, EVP_PKEY_cmp(X509_get_pubkey(cacert.get()), X509_get_pubkey(newCACert.get())));
202+
203+
BOOST_CHECK_MESSAGE(1 == X509_verify(newCACert.get(), newCAPrivateKey.get()), "Failed to verify new CA certificate: " << ERR_error_string(ERR_get_error(), nullptr));
204+
BOOST_CHECK_MESSAGE(1 > X509_verify(newCACert.get(), caprivatekey.get()), "New CA certificate should not be verifiable with the old private key");
205+
BOOST_CHECK_MESSAGE(1 > X509_verify(cacert.get(), newCAPrivateKey.get()), "Old CA certificate should not be verifiable with the new private key");
206+
207+
BOOST_CHECK_EQUAL(true, IsCaUptodate(newCACert.get()));
208+
BOOST_CHECK_EQUAL(true, VerifyCertificate(newCACert, newCACert, String())); // Self-signed CA!
209+
// Verification should fail because the leaf certificate was signed by the old CA.
210+
BOOST_CHECK_THROW(VerifyCertificate(newCACert, cert, String()), openssl_error);
211+
212+
// Renew the leaf certificate and check whether verification works with the new CA.
213+
std::shared_ptr<EVP_PKEY> pubkey(X509_get_pubkey(cert.get()), EVP_PKEY_free);
214+
auto leafSubject(X509_get_subject_name(cert.get()));
215+
auto newCert(CreateCertIcingaCA(pubkey.get(), leafSubject, false));
216+
BOOST_REQUIRE(newCert);
217+
BOOST_CHECK_EQUAL(true, IsCertUptodate(newCert));
218+
BOOST_CHECK_EQUAL(true, VerifyCertificate(newCACert, newCert, String()));
219+
// Verification should fail because the new leaf certificate was signed by the newly generated CA.
220+
BOOST_CHECK_THROW(VerifyCertificate(cacert, newCert, String()), openssl_error);
133221
}
134222

135223
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)