Implement the "pki request" and "pki ticket" commands

refs #7244
This commit is contained in:
Gunnar Beutner 2014-10-16 12:27:09 +02:00
parent 3fd20d56aa
commit f433679b13
13 changed files with 197 additions and 116 deletions

View File

@ -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 = "<secret key>"
}

View File

@ -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);
}
/**

View File

@ -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();
}

View File

@ -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:

View File

@ -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);

View File

@ -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<SSL_CTX> 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<X509> 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<char *>(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<X509> 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<X509>(cert, X509_free);
}
String GetIcingaCADir(void)
{
return Application::GetLocalStateDir() + "/lib/icinga2/ca";
}
shared_ptr<X509> 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<char *>(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<X509>();
}
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<X509>();
}
BIO_free(cakeybio);
String cacertfile = cadir + "/ca.crt";
shared_ptr<X509> 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<X509>& 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<const unsigned char *>(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;

View File

@ -35,12 +35,16 @@ namespace icinga
{
void I2_BASE_API InitializeOpenSSL(void);
shared_ptr<SSL_CTX> I2_BASE_API MakeSSLContext(const String& pubkey, const String& privkey, const String& cakey);
shared_ptr<SSL_CTX> I2_BASE_API MakeSSLContext(const String& pubkey, const String& privkey, const String& cakey = String());
void I2_BASE_API AddCRLToSSLContext(const shared_ptr<SSL_CTX>& context, const String& crlPath);
String I2_BASE_API GetCertificateCN(const shared_ptr<X509>& certificate);
shared_ptr<X509> 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<X509> 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<X509>& cert);
shared_ptr<X509> 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 { };

View File

@ -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

View File

@ -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<X509> 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<char *>(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<char *>(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;
}

View File

@ -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<Dictionary>();
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<X509> cert = origin.FromClient->GetStream()->GetPeerCertificate();
EVP_PKEY *pubkey = X509_get_pubkey(cert.get());
X509_NAME *subject = X509_get_subject_name(cert.get());
shared_ptr<X509> newcert = CreateCertIcingaCA(pubkey, subject);
result->Set("cert", CertificateToString(newcert));
String cacertfile = GetIcingaCADir() + "/ca.crt";
shared_ptr<X509> cacert = GetX509Certificate(cacertfile);
result->Set("ca", CertificateToString(cacert));
return result;
}

View File

@ -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;

View File

@ -18,6 +18,8 @@ class ApiListener : DynamicObject
[config] bool accept_config;
[config] String ticket_salt;
[state] double log_message_timestamp;
String identity;

View File

@ -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 {