From f433679b13c9feb826e3f7f6657ccf99503a8331 Mon Sep 17 00:00:00 2001 From: Gunnar Beutner Date: Thu, 16 Oct 2014 12:27:09 +0200 Subject: [PATCH] Implement the "pki request" and "pki ticket" commands refs #7244 --- etc/icinga2/features-available/api.conf | 2 + lib/base/application.cpp | 13 +-- lib/base/scriptvariable.cpp | 8 +- lib/base/scriptvariable.hpp | 2 +- lib/base/tlsstream.cpp | 20 +++- lib/base/tlsutility.cpp | 130 ++++++++++++++++++------ lib/base/tlsutility.hpp | 8 +- lib/cli/CMakeLists.txt | 4 +- lib/cli/pkisigncsrcommand.cpp | 68 +------------ lib/remote/apiclient.cpp | 44 +++++++- lib/remote/apiclient.hpp | 8 +- lib/remote/apilistener.ti | 2 + lib/remote/remote-type.conf | 4 +- 13 files changed, 197 insertions(+), 116 deletions(-) diff --git a/etc/icinga2/features-available/api.conf b/etc/icinga2/features-available/api.conf index 104d82c75..44a43f3fd 100644 --- a/etc/icinga2/features-available/api.conf +++ b/etc/icinga2/features-available/api.conf @@ -6,4 +6,6 @@ object ApiListener "api" { cert_path = SysconfDir + "/icinga2/pki/" + NodeName + ".crt" key_path = SysconfDir + "/icinga2/pki/" + NodeName + ".key" ca_path = SysconfDir + "/icinga2/pki/ca.crt" + + //ticket_salt = "" } diff --git a/lib/base/application.cpp b/lib/base/application.cpp index 053844dc0..acb6d751a 100644 --- a/lib/base/application.cpp +++ b/lib/base/application.cpp @@ -918,7 +918,7 @@ void Application::DeclareLocalStateDir(const String& path) */ String Application::GetZonesDir(void) { - return ScriptVariable::Get("ZonesDir"); + return ScriptVariable::Get("ZonesDir", &Empty); } /** @@ -938,7 +938,8 @@ void Application::DeclareZonesDir(const String& path) */ String Application::GetPkgDataDir(void) { - return ScriptVariable::Get("PkgDataDir"); + String defaultValue = ""; + return ScriptVariable::Get("PkgDataDir", &Empty); } /** @@ -958,7 +959,7 @@ void Application::DeclarePkgDataDir(const String& path) */ String Application::GetIncludeConfDir(void) { - return ScriptVariable::Get("IncludeConfDir"); + return ScriptVariable::Get("IncludeConfDir", &Empty); } /** @@ -978,7 +979,7 @@ void Application::DeclareIncludeConfDir(const String& path) */ String Application::GetStatePath(void) { - return ScriptVariable::Get("StatePath"); + return ScriptVariable::Get("StatePath", &Empty); } /** @@ -998,7 +999,7 @@ void Application::DeclareStatePath(const String& path) */ String Application::GetObjectsPath(void) { - return ScriptVariable::Get("ObjectsPath"); + return ScriptVariable::Get("ObjectsPath", &Empty); } /** @@ -1018,7 +1019,7 @@ void Application::DeclareObjectsPath(const String& path) */ String Application::GetPidPath(void) { - return ScriptVariable::Get("PidPath"); + return ScriptVariable::Get("PidPath", &Empty); } /** diff --git a/lib/base/scriptvariable.cpp b/lib/base/scriptvariable.cpp index 55f0366dd..5174ad5eb 100644 --- a/lib/base/scriptvariable.cpp +++ b/lib/base/scriptvariable.cpp @@ -51,12 +51,16 @@ Value ScriptVariable::GetData(void) const return m_Data; } -Value ScriptVariable::Get(const String& name) +Value ScriptVariable::Get(const String& name, const Value *defaultValue) { ScriptVariable::Ptr sv = GetByName(name); - if (!sv) + if (!sv) { + if (defaultValue) + return *defaultValue; + BOOST_THROW_EXCEPTION(std::invalid_argument("Tried to access undefined script variable '" + name + "'")); + } return sv->GetData(); } diff --git a/lib/base/scriptvariable.hpp b/lib/base/scriptvariable.hpp index 5112afca9..491e8b05a 100644 --- a/lib/base/scriptvariable.hpp +++ b/lib/base/scriptvariable.hpp @@ -48,7 +48,7 @@ public: static ScriptVariable::Ptr GetByName(const String& name); static void Unregister(const String& name); - static Value Get(const String& name); + static Value Get(const String& name, const Value *defaultValue = NULL); static ScriptVariable::Ptr Set(const String& name, const Value& value, bool overwrite = true, bool make_const = false); private: diff --git a/lib/base/tlsstream.cpp b/lib/base/tlsstream.cpp index b70e18cd6..347dd67c7 100644 --- a/lib/base/tlsstream.cpp +++ b/lib/base/tlsstream.cpp @@ -161,7 +161,15 @@ size_t TlsStream::Read(void *buffer, size_t count) std::ostringstream msgbuf; char errbuf[120]; - m_Socket->Poll(true, false); + bool want_read; + + { + boost::mutex::scoped_lock lock(m_SSLLock); + want_read = SSL_want_read(m_SSL.get()); + } + + if (want_read) + m_Socket->Poll(true, false); boost::mutex::scoped_lock alock(m_IOActionLock); @@ -213,7 +221,15 @@ void TlsStream::Write(const void *buffer, size_t count) std::ostringstream msgbuf; char errbuf[120]; - m_Socket->Poll(false, true); + bool want_write; + + { + boost::mutex::scoped_lock lock(m_SSLLock); + want_write = SSL_want_write(m_SSL.get()); + } + + if (want_write) + m_Socket->Poll(false, true); boost::mutex::scoped_lock alock(m_IOActionLock); diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index 4c1000495..6d8262cdb 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -21,6 +21,7 @@ #include "base/convert.hpp" #include "base/logger_fwd.hpp" #include "base/context.hpp" +#include "base/application.hpp" namespace icinga { @@ -110,29 +111,31 @@ shared_ptr MakeSSLContext(const String& pubkey, const String& privkey, << errinfo_openssl_error(ERR_peek_error())); } - if (!SSL_CTX_load_verify_locations(sslContext.get(), cakey.CStr(), NULL)) { - msgbuf << "Error loading and verifying locations in ca key file '" << cakey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; - Log(LogCritical, "SSL", msgbuf.str()); - BOOST_THROW_EXCEPTION(openssl_error() - << boost::errinfo_api_function("SSL_CTX_load_verify_locations") - << errinfo_openssl_error(ERR_peek_error()) - << boost::errinfo_file_name(cakey)); + if (!cakey.IsEmpty()) { + if (!SSL_CTX_load_verify_locations(sslContext.get(), cakey.CStr(), NULL)) { + msgbuf << "Error loading and verifying locations in ca key file '" << cakey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; + Log(LogCritical, "SSL", msgbuf.str()); + BOOST_THROW_EXCEPTION(openssl_error() + << boost::errinfo_api_function("SSL_CTX_load_verify_locations") + << errinfo_openssl_error(ERR_peek_error()) + << boost::errinfo_file_name(cakey)); + } + + STACK_OF(X509_NAME) *cert_names; + + cert_names = SSL_load_client_CA_file(cakey.CStr()); + if (cert_names == NULL) { + msgbuf << "Error loading client ca key file '" << cakey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; + Log(LogCritical, "SSL", msgbuf.str()); + BOOST_THROW_EXCEPTION(openssl_error() + << boost::errinfo_api_function("SSL_load_client_CA_file") + << errinfo_openssl_error(ERR_peek_error()) + << boost::errinfo_file_name(cakey)); + } + + SSL_CTX_set_client_CA_list(sslContext.get(), cert_names); } - STACK_OF(X509_NAME) *cert_names; - - cert_names = SSL_load_client_CA_file(cakey.CStr()); - if (cert_names == NULL) { - msgbuf << "Error loading client ca key file '" << cakey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; - Log(LogCritical, "SSL", msgbuf.str()); - BOOST_THROW_EXCEPTION(openssl_error() - << boost::errinfo_api_function("SSL_load_client_CA_file") - << errinfo_openssl_error(ERR_peek_error()) - << boost::errinfo_file_name(cakey)); - } - - SSL_CTX_set_client_CA_list(sslContext.get(), cert_names); - return sslContext; } @@ -268,7 +271,7 @@ int MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile, X509_NAME *subject = X509_NAME_new(); X509_NAME_add_entry_by_txt(subject, "CN", MBSTRING_ASC, (unsigned char *)cn.CStr(), -1, -1, 0); - X509 *cert = CreateCert(key, subject, subject, key, ca); + shared_ptr cert = CreateCert(key, subject, subject, key, ca); X509_NAME_free(subject); @@ -276,10 +279,8 @@ int MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile, bio = BIO_new(BIO_s_file()); BIO_write_filename(bio, const_cast(certfile.CStr())); - PEM_write_bio_X509(bio, cert); + PEM_write_bio_X509(bio, cert.get()); BIO_free(bio); - - X509_free(cert); } if (!csrfile.IsEmpty()) { @@ -311,7 +312,7 @@ int MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile, return 1; } -X509 *CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca, const String& serialfile) +shared_ptr CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca, const String& serialfile) { X509 *cert = X509_new(); ASN1_INTEGER_set(X509_get_serialNumber(cert), 1); @@ -337,7 +338,79 @@ X509 *CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PK X509_sign(cert, cakey, EVP_sha1()); - return cert; + return shared_ptr(cert, X509_free); +} + +String GetIcingaCADir(void) +{ + return Application::GetLocalStateDir() + "/lib/icinga2/ca"; +} + +shared_ptr CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject) +{ + std::stringstream msgbuf; + char errbuf[120]; + + String cadir = GetIcingaCADir(); + + String cakeyfile = cadir + "/ca.key"; + + RSA *rsa; + + BIO *cakeybio = BIO_new_file(const_cast(cakeyfile.CStr()), "r"); + + if (!cakeybio) { + msgbuf << "Could not open CA key file '" << cakeyfile << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; + Log(LogCritical, "SSL", msgbuf.str()); + return shared_ptr(); + } + + rsa = PEM_read_bio_RSAPrivateKey(cakeybio, NULL, NULL, NULL); + + if (!rsa) { + msgbuf << "Could not read RSA key from CA key file '" << cakeyfile << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; + Log(LogCritical, "SSL", msgbuf.str()); + return shared_ptr(); + } + + BIO_free(cakeybio); + + String cacertfile = cadir + "/ca.crt"; + + shared_ptr cacert = GetX509Certificate(cacertfile); + + EVP_PKEY *privkey = EVP_PKEY_new(); + EVP_PKEY_assign_RSA(privkey, rsa); + + return CreateCert(pubkey, subject, X509_get_subject_name(cacert.get()), privkey, false, cadir + "/serial.txt"); +} + +String CertificateToString(const shared_ptr& cert) +{ + BIO *mem = BIO_new(BIO_s_mem()); + PEM_write_bio_X509(mem, cert.get()); + + char *data; + long len = BIO_get_mem_data(mem, &data); + + String result = String(data, data + len); + + BIO_free(mem); + + return result; +} + +String PBKDF2_SHA512(const String& password, const String& salt, int iterations) +{ + unsigned char digest[SHA512_DIGEST_LENGTH]; + PKCS5_PBKDF2_HMAC(password.CStr(), password.GetLength(), reinterpret_cast(salt.CStr()), salt.GetLength(), + iterations, EVP_sha512(), sizeof(digest), digest); + + char output[SHA512_DIGEST_LENGTH*2+1]; + for (int i = 0; i < 32; i++) + sprintf(output + 2 * i, "%02x", digest[i]); + + return output; } String SHA256(const String& s) @@ -371,9 +444,8 @@ String SHA256(const String& s) << errinfo_openssl_error(ERR_peek_error())); } - int i; char output[SHA256_DIGEST_LENGTH*2+1]; - for (i = 0; i < 32; i++) + for (int i = 0; i < 32; i++) sprintf(output + 2 * i, "%02x", digest[i]); return output; diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp index a2e8b6500..bd272b88f 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -35,12 +35,16 @@ namespace icinga { void I2_BASE_API InitializeOpenSSL(void); -shared_ptr I2_BASE_API MakeSSLContext(const String& pubkey, const String& privkey, const String& cakey); +shared_ptr I2_BASE_API MakeSSLContext(const String& pubkey, const String& privkey, const String& cakey = String()); void I2_BASE_API AddCRLToSSLContext(const shared_ptr& context, const String& crlPath); String I2_BASE_API GetCertificateCN(const shared_ptr& certificate); shared_ptr I2_BASE_API GetX509Certificate(const String& pemfile); int I2_BASE_API MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile = String(), const String& certfile = String(), bool ca = false); -X509 * I2_BASE_API CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca, const String& serialfile = String()); +shared_ptr I2_BASE_API CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca, const String& serialfile = String()); +String I2_BASE_API GetIcingaCADir(void); +String I2_BASE_API CertificateToString(const shared_ptr& cert); +shared_ptr I2_BASE_API CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject); +String I2_BASE_API PBKDF2_SHA512(const String& password, const String& salt, int iterations); String I2_BASE_API SHA256(const String& s); class I2_BASE_API openssl_error : virtual public std::exception, virtual public boost::exception { }; diff --git a/lib/cli/CMakeLists.txt b/lib/cli/CMakeLists.txt index b3c55551c..0347ab827 100644 --- a/lib/cli/CMakeLists.txt +++ b/lib/cli/CMakeLists.txt @@ -18,7 +18,7 @@ set(cli_SOURCES featureenablecommand.cpp featuredisablecommand.cpp featurelistcommand.cpp objectlistcommand.cpp - pkinewcacommand.cpp pkinewcertcommand.cpp pkisigncsrcommand.cpp + pkinewcacommand.cpp pkinewcertcommand.cpp pkisigncsrcommand.cpp pkirequestcommand.cpp pkiticketcommand.cpp daemoncommand.cpp ) @@ -28,7 +28,7 @@ endif() add_library(cli SHARED ${cli_SOURCES}) -target_link_libraries(cli ${Boost_LIBRARIES} base config) +target_link_libraries(cli ${Boost_LIBRARIES} base config remote) set_target_properties ( cli PROPERTIES diff --git a/lib/cli/pkisigncsrcommand.cpp b/lib/cli/pkisigncsrcommand.cpp index 2019c9828..d5af4d906 100644 --- a/lib/cli/pkisigncsrcommand.cpp +++ b/lib/cli/pkisigncsrcommand.cpp @@ -68,73 +68,11 @@ int PKISignCSRCommand::Run(const boost::program_options::variables_map& vm, cons BIO_free(csrbio); - String cadir = Application::GetLocalStateDir() + "/lib/icinga2/ca"; + shared_ptr cert = CreateCertIcingaCA(X509_REQ_get_pubkey(req), X509_REQ_get_subject_name(req)); - String cakeyfile = cadir + "/ca.key"; + X509_REQ_free(req); - RSA *rsa; - - BIO *cakeybio = BIO_new_file(const_cast(cakeyfile.CStr()), "r"); - - if (!cakeybio) { - msgbuf << "Could not open CA key file '" << cakeyfile << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; - Log(LogCritical, "SSL", msgbuf.str()); - return 1; - } - - rsa = PEM_read_bio_RSAPrivateKey(cakeybio, NULL, NULL, NULL); - - if (!rsa) { - msgbuf << "Could not read RSA key from CA key file '" << cakeyfile << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; - Log(LogCritical, "SSL", msgbuf.str()); - return 1; - } - - BIO_free(cakeybio); - - String cacertfile = cadir + "/ca.crt"; - - BIO *cacertbio = BIO_new_file(const_cast(cacertfile.CStr()), "r"); - - if (!cacertbio) { - msgbuf << "Could not open CA certificate file '" << cakeyfile << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; - Log(LogCritical, "SSL", msgbuf.str()); - return 1; - } - - X509 *cacert = PEM_read_bio_X509(cacertbio, NULL, NULL, NULL); - - if (!cacert) { - msgbuf << "Could not read X509 certificate from CA certificate file '" << cakeyfile << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; - Log(LogCritical, "SSL", msgbuf.str()); - return 1; - } - - BIO_free(cacertbio); - - EVP_PKEY *privkey = EVP_PKEY_new(); - EVP_PKEY_assign_RSA(privkey, rsa); - - EVP_PKEY *pubkey = X509_REQ_get_pubkey(req); - - X509 *cert = CreateCert(pubkey, X509_REQ_get_subject_name(req), X509_get_subject_name(cacert), privkey, false); - - EVP_PKEY_free(pubkey); - X509_free(cacert); - - BIO *certbio = BIO_new_fp(stdout, BIO_NOCLOSE); - - if (!PEM_write_bio_X509(certbio, cert)) { - BIO_free(certbio); - - msgbuf << "Could not write X509 certificate: " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; - Log(LogCritical, "SSL", msgbuf.str()); - return 1; - } - - X509_free(cert); - - BIO_free(certbio); + std::cout << CertificateToString(cert); return 0; } diff --git a/lib/remote/apiclient.cpp b/lib/remote/apiclient.cpp index 9f57aa72e..68dd690eb 100644 --- a/lib/remote/apiclient.cpp +++ b/lib/remote/apiclient.cpp @@ -31,8 +31,10 @@ using namespace icinga; static Value SetLogPositionHandler(const MessageOrigin& origin, const Dictionary::Ptr& params); REGISTER_APIFUNCTION(SetLogPosition, log, &SetLogPositionHandler); +static Value RequestCertificateHandler(const MessageOrigin& origin, const Dictionary::Ptr& params); +REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler); -ApiClient::ApiClient(const String& identity, bool authenticated, const Stream::Ptr& stream, ConnectionRole role) +ApiClient::ApiClient(const String& identity, bool authenticated, const TlsStream::Ptr& stream, ConnectionRole role) : m_Identity(identity), m_Authenticated(authenticated), m_Stream(stream), m_Role(role), m_Seen(Utility::GetTime()) { if (authenticated) @@ -60,7 +62,7 @@ Endpoint::Ptr ApiClient::GetEndpoint(void) const return m_Endpoint; } -Stream::Ptr ApiClient::GetStream(void) const +TlsStream::Ptr ApiClient::GetStream(void) const { return m_Stream; } @@ -220,3 +222,41 @@ Value SetLogPositionHandler(const MessageOrigin& origin, const Dictionary::Ptr& return Empty; } + +Value RequestCertificateHandler(const MessageOrigin& origin, const Dictionary::Ptr& params) +{ + if (!params) + return Empty; + + ApiListener::Ptr listener = ApiListener::GetInstance(); + String salt = listener->GetTicketSalt(); + + Dictionary::Ptr result = make_shared(); + + if (salt.IsEmpty()) { + result->Set("error", "Ticket salt is not configured."); + return result; + } + + String ticket = params->Get("ticket"); + String realTicket = PBKDF2_SHA512(origin.FromClient->GetIdentity(), salt, 50000); + + if (ticket != realTicket) { + result->Set("error", "Invalid ticket."); + return result; + } + + shared_ptr cert = origin.FromClient->GetStream()->GetPeerCertificate(); + + EVP_PKEY *pubkey = X509_get_pubkey(cert.get()); + X509_NAME *subject = X509_get_subject_name(cert.get()); + + shared_ptr newcert = CreateCertIcingaCA(pubkey, subject); + result->Set("cert", CertificateToString(newcert)); + + String cacertfile = GetIcingaCADir() + "/ca.crt"; + shared_ptr cacert = GetX509Certificate(cacertfile); + result->Set("ca", CertificateToString(cacert)); + + return result; +} diff --git a/lib/remote/apiclient.hpp b/lib/remote/apiclient.hpp index 480eb576e..9c05a4f1e 100644 --- a/lib/remote/apiclient.hpp +++ b/lib/remote/apiclient.hpp @@ -21,7 +21,7 @@ #define APICLIENT_H #include "remote/endpoint.hpp" -#include "base/stream.hpp" +#include "base/tlsstream.hpp" #include "base/timer.hpp" #include "base/workqueue.hpp" #include "remote/i2-remote.hpp" @@ -45,14 +45,14 @@ class I2_REMOTE_API ApiClient : public Object public: DECLARE_PTR_TYPEDEFS(ApiClient); - ApiClient(const String& identity, bool authenticated, const Stream::Ptr& stream, ConnectionRole role); + ApiClient(const String& identity, bool authenticated, const TlsStream::Ptr& stream, ConnectionRole role); void Start(void); String GetIdentity(void) const; bool IsAuthenticated(void) const; Endpoint::Ptr GetEndpoint(void) const; - Stream::Ptr GetStream(void) const; + TlsStream::Ptr GetStream(void) const; ConnectionRole GetRole(void) const; void Disconnect(void); @@ -64,7 +64,7 @@ private: String m_Identity; bool m_Authenticated; Endpoint::Ptr m_Endpoint; - Stream::Ptr m_Stream; + TlsStream::Ptr m_Stream; ConnectionRole m_Role; double m_Seen; diff --git a/lib/remote/apilistener.ti b/lib/remote/apilistener.ti index 75a3e1195..90b29bbc5 100644 --- a/lib/remote/apilistener.ti +++ b/lib/remote/apilistener.ti @@ -18,6 +18,8 @@ class ApiListener : DynamicObject [config] bool accept_config; + [config] String ticket_salt; + [state] double log_message_timestamp; String identity; diff --git a/lib/remote/remote-type.conf b/lib/remote/remote-type.conf index c59a8e918..ae1b97fa1 100644 --- a/lib/remote/remote-type.conf +++ b/lib/remote/remote-type.conf @@ -32,7 +32,9 @@ %attribute %string "bind_host", %attribute %string "bind_port", - %attribute %number "accept_config" + %attribute %number "accept_config", + + %attribute %string "ticket_salt" } %type Endpoint { -- 2.40.0