diff --git a/CHANGELOG.md b/CHANGELOG.md index 724d320d9..726d50ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,25 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic ## 2.11.8 (2020-12-15) -Version 2.11.8 mainly focuses on resolving issues with high load on Windows regarding the config sync +Version 2.11.8 resolves a security vulnerability with revoked certificates being +renewed automatically ignoring the CRL. + +This version also resolves issues with high load on Windows regarding the config sync and not being able to disable/enable Icinga 2 features over the API. +### Security + +* Fix that revoked certificates due for renewal will automatically be renewed ignoring the CRL (CVE-2020-29663) + +When a CRL is specified in the ApiListener configuration, Icinga 2 only used it +when connections were established so far, but not when a certificate is requested. +This allows a node to automatically renew a revoked certificate if it meets the +other conditions for auto renewal (issued before 2017 or expires in less than 30 days). + +Because Icinga 2 currently (v2.12.3 and earlier) uses a validity duration of 15 years, +this only affects setups with external certificate signing and revoked certificates +that expire in less then 30 days. + ### Bugfixes * Improve config sync locking - resolves high load issues on Windows #8510 diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index c08090cc7..569016b96 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -236,15 +236,26 @@ void SetTlsProtocolminToSSLContext(const std::shared_ptr& context, const String& crlPath) { - char errbuf[256]; X509_STORE *x509_store = SSL_CTX_get_cert_store(context->native_handle()); + AddCRLToSSLContext(x509_store, crlPath); +} + +/** + * Loads a CRL and appends its certificates to the specified OpenSSL X509 store. + * + * @param context The SSL context. + * @param crlPath The path to the CRL file. + */ +void AddCRLToSSLContext(X509_STORE *x509_store, const String& crlPath) +{ + char errbuf[256]; X509_LOOKUP *lookup; lookup = X509_STORE_add_lookup(x509_store, X509_LOOKUP_file()); @@ -821,7 +832,7 @@ String RandomString(int length) return result; } -bool VerifyCertificate(const std::shared_ptr& caCertificate, const std::shared_ptr& certificate) +bool VerifyCertificate(const std::shared_ptr &caCertificate, const std::shared_ptr &certificate, const String& crlFile) { X509_STORE *store = X509_STORE_new(); @@ -830,6 +841,10 @@ bool VerifyCertificate(const std::shared_ptr& caCertificate, const std::sh X509_STORE_add_cert(store, caCertificate.get()); + if (!crlFile.IsEmpty()) { + AddCRLToSSLContext(store, crlFile); + } + X509_STORE_CTX *csc = X509_STORE_CTX_new(); X509_STORE_CTX_init(csc, store, certificate.get(), nullptr); diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp index ac4e642b6..746a32f66 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -22,10 +22,13 @@ namespace icinga void InitializeOpenSSL(); -std::shared_ptr MakeAsioSslContext(const String& pubkey = String(), const String& privkey = String(), const String& cakey = String()); -void AddCRLToSSLContext(const std::shared_ptr& context, const String& crlPath); -void SetCipherListToSSLContext(const std::shared_ptr& context, const String& cipherList); -void SetTlsProtocolminToSSLContext(const std::shared_ptr& context, const String& tlsProtocolmin); +String GetOpenSSLVersion(); + +Shared::Ptr MakeAsioSslContext(const String& pubkey = String(), const String& privkey = String(), const String& cakey = String()); +void AddCRLToSSLContext(const Shared::Ptr& context, const String& crlPath); +void AddCRLToSSLContext(X509_STORE *x509_store, const String& crlPath); +void SetCipherListToSSLContext(const Shared::Ptr& context, const String& cipherList); +void SetTlsProtocolminToSSLContext(const Shared::Ptr& context, const String& tlsProtocolmin); String GetCertificateCN(const std::shared_ptr& certificate); std::shared_ptr GetX509Certificate(const String& pemfile); @@ -45,7 +48,11 @@ String SHA1(const String& s, bool binary = false); String SHA256(const String& s); String RandomString(int length); -bool VerifyCertificate(const std::shared_ptr& caCertificate, const std::shared_ptr& certificate); +bool VerifyCertificate(const std::shared_ptr& caCertificate, const std::shared_ptr& certificate, const String& crlFile); +bool IsCa(const std::shared_ptr& cacert); +int GetCertificateVersion(const std::shared_ptr& cert); +String GetSignatureAlgorithm(const std::shared_ptr& cert); +Array::Ptr GetSubjectAltNames(const std::shared_ptr& cert); class openssl_error : virtual public std::exception, virtual public boost::exception { }; diff --git a/lib/remote/jsonrpcconnection-pki.cpp b/lib/remote/jsonrpcconnection-pki.cpp index aa7ebcef0..baa115d69 100644 --- a/lib/remote/jsonrpcconnection-pki.cpp +++ b/lib/remote/jsonrpcconnection-pki.cpp @@ -53,11 +53,27 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona String cn = GetCertificateCN(cert); - bool signedByCA = VerifyCertificate(cacert, cert); + bool signedByCA = false; - Log(LogInformation, "JsonRpcConnection") - << "Received certificate request for CN '" << cn << "'" - << (signedByCA ? "" : " not") << " signed by our CA."; + { + Log logmsg(LogInformation, "JsonRpcConnection"); + logmsg << "Received certificate request for CN '" << cn << "'"; + + try { + signedByCA = VerifyCertificate(cacert, cert, listener->GetCrlPath()); + if (!signedByCA) { + logmsg << " not"; + } + logmsg << " signed by our CA."; + } catch (const std::exception &ex) { + logmsg << " not signed by our CA"; + if (const unsigned long *openssl_code = boost::get_error_info(ex)) { + logmsg << ": " << X509_verify_cert_error_string(long(*openssl_code)) << " (code " << *openssl_code << ")"; + } else { + logmsg << "."; + } + } + } if (signedByCA) { time_t now; @@ -199,12 +215,14 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona * this ensures that the CA we have in /var/lib/icinga2/ca matches the one * we're using for cluster connections (there's no point in sending a client * a certificate it wouldn't be able to use to connect to us anyway) */ - if (!VerifyCertificate(cacert, newcert)) { - Log(LogWarning, "JsonRpcConnection") - << "The CA in '" << listener->GetDefaultCaPath() << "' does not match the CA which Icinga uses " - << "for its own cluster connections. This is most likely a configuration problem."; - goto delayed_request; - } + try { + if (!VerifyCertificate(cacert, newcert, listener->GetCrlPath())) { + Log(LogWarning, "JsonRpcConnection") + << "The CA in '" << listener->GetDefaultCaPath() << "' does not match the CA which Icinga uses " + << "for its own cluster connections. This is most likely a configuration problem."; + goto delayed_request; + } + } catch (const std::exception&) { } /* Swallow the exception on purpose, cacert will never be a non-CA certificate. */ /* Send the signed certificate update. */ Log(LogInformation, "JsonRpcConnection")