|
1 | 1 | /* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
|
2 | 2 |
|
| 3 | +#include "base/configuration.hpp" |
3 | 4 | #include "base/tlsutility.hpp"
|
| 5 | +#include "base/utility.hpp" |
| 6 | +#include "remote/apilistener.hpp" |
| 7 | +#include "remote/pkiutility.hpp" |
4 | 8 | #include <BoostTestTargetConfig.h>
|
| 9 | +#include <boost/filesystem/operations.hpp> |
| 10 | +#include <boost/filesystem/path.hpp> |
5 | 11 | #include <functional>
|
6 | 12 | #include <memory>
|
7 |
| -#include <openssl/asn1.h> |
8 |
| -#include <openssl/bn.h> |
9 | 13 | #include <openssl/evp.h>
|
10 |
| -#include <openssl/obj_mac.h> |
11 |
| -#include <openssl/rsa.h> |
12 | 14 | #include <openssl/x509.h>
|
13 | 15 | #include <utility>
|
14 | 16 | #include <vector>
|
15 | 17 |
|
16 | 18 | using namespace icinga;
|
17 | 19 |
|
18 |
| -static EVP_PKEY* GenKeypair() |
| 20 | +struct ConfigurationConstantsFixture |
19 | 21 | {
|
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 | + } |
53 | 31 |
|
54 |
| - return std::shared_ptr<X509>(cert, X509_free); |
55 |
| -} |
| 32 | + ~ConfigurationConstantsFixture() |
| 33 | + { |
| 34 | + Configuration::DataDir = PreviousDataDir; |
| 35 | + Utility::RemoveDirRecursive(TmpDir); |
| 36 | + } |
| 37 | +}; |
56 | 38 |
|
57 | 39 | static const long l_2016 = 1480000000; // Thu Nov 24 15:06:40 UTC 2016
|
58 | 40 | static const long l_2017 = 1490000000; // Mon Mar 20 08:53:20 UTC 2017
|
@@ -85,51 +67,128 @@ BOOST_AUTO_TEST_CASE(sha1)
|
85 | 67 | }
|
86 | 68 | }
|
87 | 69 |
|
88 |
| -BOOST_AUTO_TEST_CASE(iscauptodate_ok) |
| 70 | +BOOST_AUTO_TEST_CASE(create_verify_ca) |
89 | 71 | {
|
90 |
| - auto key (GenKeypair()); |
| 72 | + ConfigurationConstantsFixture f; |
91 | 73 |
|
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 |
| -} |
| 74 | + BOOST_CHECK_EQUAL(0, PkiUtility::NewCa()); |
97 | 75 |
|
98 |
| -BOOST_AUTO_TEST_CASE(iscauptodate_expiring) |
99 |
| -{ |
100 |
| - auto key (GenKeypair()); |
| 76 | + auto cacert(GetX509Certificate(ApiListener::GetCaDir()+"/ca.crt")); |
| 77 | + if (OPENSSL_VERSION_NUMBER >= 0x10100000L) { |
| 78 | + // OpenSSL 1.1.x provides https://www.openssl.org/docs/man1.1.0/man3/X509_check_ca.html |
| 79 | + BOOST_CHECK_EQUAL(true, IsCa(cacert)); |
| 80 | + } else { |
| 81 | + BOOST_CHECK_THROW(IsCa(cacert), std::invalid_argument); |
| 82 | + } |
| 83 | + BOOST_CHECK_EQUAL(true, VerifyCertificate(cacert, cacert, String())); |
| 84 | + BOOST_CHECK_EQUAL(true, IsCaUptodate(cacert.get())); |
101 | 85 |
|
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 |
| -} |
| 86 | + time_t caValidUntil(time(nullptr) + ROOT_VALID_FOR); |
| 87 | + BOOST_CHECK_EQUAL(-1, X509_cmp_time(X509_get_notAfter(cacert.get()), &caValidUntil)); |
107 | 88 |
|
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 |
| -} |
| 89 | + // Set the CA certificate to expire in 100 days, i.e. less than the LEAF_VALID_FOR threshold of 397 days. |
| 90 | + BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cacert.get()), 60*60*24*100)); |
| 91 | + BOOST_CHECK_EQUAL(false, IsCaUptodate(cacert.get())); |
116 | 92 |
|
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 |
| - }))); |
| 93 | + // Even if the CA is going to expire at exactly the same time as the LEAF_VALID_FOR threshold, |
| 94 | + // it is still considered to be outdated. |
| 95 | + BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cacert.get()), 60*60*24*397)); |
| 96 | + BOOST_CHECK_EQUAL(false, IsCaUptodate(cacert.get())); |
| 97 | + |
| 98 | + // Reset the CA expiration date to the original value, i.e. 15 years. |
| 99 | + BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cacert.get()), ROOT_VALID_FOR)); |
| 100 | + BOOST_CHECK_EQUAL(true, IsCaUptodate(cacert.get())); |
124 | 101 | }
|
125 | 102 |
|
126 |
| -BOOST_AUTO_TEST_CASE(iscertuptodate_old) |
| 103 | +BOOST_AUTO_TEST_CASE(create_verify_leaf_certs) |
127 | 104 | {
|
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 |
| - }))); |
| 105 | + ConfigurationConstantsFixture f; |
| 106 | + |
| 107 | + String caDir = ApiListener::GetCaDir(); |
| 108 | + String certsDir = ApiListener::GetCertsDir(); |
| 109 | + Utility::MkDirP(certsDir, 0700); |
| 110 | + |
| 111 | + BOOST_CHECK_EQUAL(0, PkiUtility::NewCa()); |
| 112 | + |
| 113 | + auto cacert(GetX509Certificate(caDir+"/ca.crt")); |
| 114 | + BOOST_CHECK_EQUAL(true, IsCaUptodate(cacert.get())); |
| 115 | + |
| 116 | + BOOST_CHECK_EQUAL(0, PkiUtility::NewCert("example.com", certsDir+"example.key", certsDir+"example.csr", certsDir+"example.crt")); |
| 117 | + BOOST_CHECK_EQUAL(0, PkiUtility::SignCsr(certsDir+"example.csr", certsDir+"example.crt")); |
| 118 | + |
| 119 | + auto cert(GetX509Certificate(certsDir+"/example.crt")); |
| 120 | + if (OPENSSL_VERSION_NUMBER >= 0x10100000L) { |
| 121 | + BOOST_CHECK_EQUAL(false, IsCa(cert)); |
| 122 | + } else { |
| 123 | + BOOST_CHECK_THROW(IsCa(cert), std::invalid_argument); |
| 124 | + } |
| 125 | + BOOST_CHECK_EQUAL(true, IsCertUptodate(cert)); |
| 126 | + BOOST_CHECK_EQUAL(true, VerifyCertificate(cacert, cert, String())); |
| 127 | + |
| 128 | + time_t certValidUntil(time(nullptr) + LEAF_VALID_FOR); |
| 129 | + BOOST_CHECK_EQUAL(-1, X509_cmp_time(X509_get_notAfter(cert.get()), &certValidUntil)); |
| 130 | + |
| 131 | + // Set the certificate to expire in 20 days, i.e. less than the RENEW_THRESHOLD of 30 days. |
| 132 | + BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cert.get()), 60*60*24*20)); |
| 133 | + BOOST_CHECK_EQUAL(false, IsCertUptodate(cert)); |
| 134 | + |
| 135 | + // Check whether expired certificates are correctly detected and verification fails. |
| 136 | + BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cert.get()), -10)); |
| 137 | + BOOST_CHECK_EQUAL(false, IsCertUptodate(cert)); |
| 138 | + BOOST_CHECK_THROW(VerifyCertificate(cacert, cert, String()), openssl_error); |
| 139 | + |
| 140 | + // Reset the certificate expiration date to the original value, i.e. 397 days. |
| 141 | + BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cert.get()), LEAF_VALID_FOR)); |
| 142 | + BOOST_CHECK_EQUAL(true, IsCertUptodate(cert)); |
| 143 | + BOOST_CHECK_EQUAL(true, VerifyCertificate(cacert, cert, String())); |
| 144 | + |
| 145 | + // Set the certificate validity start date to 2016, all certificates created before 2017 are considered outdated. |
| 146 | + BOOST_CHECK(X509_gmtime_adj(X509_get_notBefore(cert.get()), -(time(nullptr)-l_2016))); |
| 147 | + BOOST_CHECK_EQUAL(false, IsCertUptodate(cert)); |
| 148 | + BOOST_CHECK_EQUAL(true, VerifyCertificate(cacert, cert, String())); |
| 149 | + |
| 150 | + // Reset the certificate validity start date to the least acceptable value, i.e. 2017. |
| 151 | + BOOST_CHECK(X509_gmtime_adj(X509_get_notBefore(cert.get()), -(time(nullptr)-l_2017))); |
| 152 | + BOOST_CHECK_EQUAL(true, IsCertUptodate(cert)); |
| 153 | + BOOST_CHECK_EQUAL(true, VerifyCertificate(cacert, cert, String())); |
| 154 | + |
| 155 | + // Even if the leaf is up-to-date, the root CA has expired 10 days ago, so verification should fail. |
| 156 | + BOOST_CHECK(X509_gmtime_adj(X509_get_notAfter(cacert.get()), -10)); |
| 157 | + BOOST_CHECK_EQUAL(false, IsCaUptodate(cacert.get())); |
| 158 | + BOOST_CHECK_THROW(VerifyCertificate(cacert, cert, String()), openssl_error); |
| 159 | + |
| 160 | + // Generate a new CA certificate to simulate a renewal and check whether verification still works. |
| 161 | + std::shared_ptr<EVP_PKEY> caPubKey(X509_get_pubkey(cacert.get()), EVP_PKEY_free); |
| 162 | + auto subject(X509_get_subject_name(cacert.get())); |
| 163 | + auto newCACert(CreateCertIcingaCA(caPubKey.get(), subject, true)); |
| 164 | + BOOST_REQUIRE(newCACert); |
| 165 | + BOOST_CHECK_EQUAL(true, IsCaUptodate(newCACert.get())); |
| 166 | + BOOST_CHECK_EQUAL(true, VerifyCertificate(newCACert, newCACert, String())); |
| 167 | + BOOST_CHECK_EQUAL(true, VerifyCertificate(newCACert, cert, String())); |
| 168 | + BOOST_CHECK_THROW(VerifyCertificate(cacert, newCACert, String()), openssl_error); |
| 169 | + |
| 170 | + // Remove the previously generated CA before regenerating a new one, PkiUtility::NewCa() would fail otherwise. |
| 171 | + Utility::RemoveDirRecursive(caDir); |
| 172 | + BOOST_CHECK_EQUAL(0, PkiUtility::NewCa()); |
| 173 | + |
| 174 | + newCACert = GetX509Certificate(caDir+"/ca.crt"); |
| 175 | + BOOST_REQUIRE(newCACert); |
| 176 | + BOOST_CHECK_NE(0, ASN1_INTEGER_cmp(X509_get_serialNumber(cacert.get()), X509_get_serialNumber(newCACert.get()))); |
| 177 | + BOOST_CHECK_NE(1, EVP_PKEY_cmp(X509_get_pubkey(cacert.get()), X509_get_pubkey(newCACert.get()))); |
| 178 | + BOOST_CHECK_EQUAL(true, IsCaUptodate(newCACert.get())); |
| 179 | + BOOST_CHECK_EQUAL(true, VerifyCertificate(newCACert, newCACert, String())); // Self-signed CA! |
| 180 | + // Verification should fail because the leaf certificate was signed by the old CA. |
| 181 | + BOOST_CHECK_THROW(VerifyCertificate(newCACert, cert, String()), openssl_error); |
| 182 | + |
| 183 | + // Renew the leaf certificate and check whether verification works with the new CA. |
| 184 | + std::shared_ptr<EVP_PKEY> pubkey(X509_get_pubkey(cert.get()), EVP_PKEY_free); |
| 185 | + auto leafSubject(X509_get_subject_name(cert.get())); |
| 186 | + auto newCert(CreateCertIcingaCA(pubkey.get(), leafSubject, false)); |
| 187 | + BOOST_REQUIRE(newCert); |
| 188 | + BOOST_CHECK_EQUAL(true, IsCertUptodate(newCert)); |
| 189 | + BOOST_CHECK_EQUAL(true, VerifyCertificate(newCACert, newCert, String())); |
| 190 | + // Verification should fail because the new leaf certificate was signed by the newly generated CA. |
| 191 | + BOOST_CHECK_THROW(VerifyCertificate(cacert, newCert, String()), openssl_error); |
133 | 192 | }
|
134 | 193 |
|
135 | 194 | BOOST_AUTO_TEST_SUITE_END()
|
0 commit comments