diff options
author | Attila Molnar <attilamolnar@hush.com> | 2013-09-24 19:43:20 +0200 |
---|---|---|
committer | Attila Molnar <attilamolnar@hush.com> | 2014-01-22 19:10:01 +0100 |
commit | 282138ad0e9ef483ec2a1606376fc5cb6d5f4cbc (patch) | |
tree | 298d5ae447a69d383b6e0fd5b045361db6c35c9b | |
parent | b68a5ad0309e49671d4c6db19384c1f82831419e (diff) |
Add the ability to have multiple SSL profiles
SSL profiles are now used instead of fixed SSL settings for everything
SSL, making it possible to use completely different settings for each
listener and outgoing connection.
Outgoing connections are broken until the next commit.
-rw-r--r-- | src/modules/extra/m_ssl_gnutls.cpp | 783 | ||||
-rw-r--r-- | src/modules/extra/m_ssl_openssl.cpp | 361 |
2 files changed, 790 insertions, 354 deletions
diff --git a/src/modules/extra/m_ssl_gnutls.cpp b/src/modules/extra/m_ssl_gnutls.cpp index 9f8e740b3..7c19925dd 100644 --- a/src/modules/extra/m_ssl_gnutls.cpp +++ b/src/modules/extra/m_ssl_gnutls.cpp @@ -26,6 +26,7 @@ #include <gnutls/x509.h> #include "modules/ssl.h" #include "modules/cap.h" +#include <memory> #if ((GNUTLS_VERSION_MAJOR > 2) || (GNUTLS_VERSION_MAJOR == 2 && GNUTLS_VERSION_MINOR > 9) || (GNUTLS_VERSION_MAJOR == 2 && GNUTLS_VERSION_MINOR == 9 && GNUTLS_VERSION_PATCH >= 8)) #define GNUTLS_HAS_MAC_GET_ID @@ -70,8 +71,6 @@ typedef gnutls_dh_params_t gnutls_dh_params; enum issl_status { ISSL_NONE, ISSL_HANDSHAKING_READ, ISSL_HANDSHAKING_WRITE, ISSL_HANDSHAKEN, ISSL_CLOSING, ISSL_CLOSED }; -static std::vector<gnutls_x509_crt_t> x509_certs; -static gnutls_x509_privkey_t x509_key; #if (GNUTLS_VERSION_MAJOR > 2 || (GNUTLS_VERSION_MAJOR == 2 && GNUTLS_VERSION_MINOR >= 12)) #define GNUTLS_NEW_CERT_CALLBACK_API typedef gnutls_retr2_st cert_cb_last_param_type; @@ -79,8 +78,6 @@ typedef gnutls_retr2_st cert_cb_last_param_type; typedef gnutls_retr_st cert_cb_last_param_type; #endif -static int cert_callback(gnutls_session_t session, const gnutls_datum_t* req_ca_rdn, int nreqs, const gnutls_pk_algorithm_t* sign_algos, int sign_algos_length, cert_cb_last_param_type* st); - class RandGen : public HandlerBase2<void, char*, size_t> { public: @@ -95,6 +92,457 @@ class RandGen : public HandlerBase2<void, char*, size_t> } }; +namespace GnuTLS +{ + class Init + { + public: + Init() { gnutls_global_init(); } + ~Init() { gnutls_global_deinit(); } + }; + + class Exception : public ModuleException + { + public: + Exception(const std::string& reason) + : ModuleException(reason) { } + }; + + void ThrowOnError(int errcode, const char* msg) + { + if (errcode < 0) + { + std::string reason = msg; + reason.append(" :").append(gnutls_strerror(errcode)); + throw Exception(reason); + } + } + + /** Used to create a gnutls_datum_t* from a std::string + */ + class Datum + { + gnutls_datum_t datum; + + public: + Datum(const std::string& dat) + { + datum.data = (unsigned char*)dat.data(); + datum.size = static_cast<unsigned int>(dat.length()); + } + + const gnutls_datum_t* get() const { return &datum; } + }; + + class Hash + { + gnutls_digest_algorithm_t hash; + + public: + // Nothing to deallocate, constructor may throw freely + Hash(const std::string& hashname) + { + // As older versions of gnutls can't do this, let's disable it where needed. +#ifdef GNUTLS_HAS_MAC_GET_ID + // As gnutls_digest_algorithm_t and gnutls_mac_algorithm_t are mapped 1:1, we can do this + // There is no gnutls_dig_get_id() at the moment, but it may come later + hash = (gnutls_digest_algorithm_t)gnutls_mac_get_id(hashname.c_str()); + if (hash == GNUTLS_DIG_UNKNOWN) + throw Exception("Unknown hash type " + hashname); + + // Check if the user is giving us something that is a valid MAC but not digest + gnutls_hash_hd_t is_digest; + if (gnutls_hash_init(&is_digest, hash) < 0) + throw Exception("Unknown hash type " + hashname); + gnutls_hash_deinit(is_digest, NULL); +#else + if (hashname == "md5") + hash = GNUTLS_DIG_MD5; + else if (hashname == "sha1") + hash = GNUTLS_DIG_SHA1; + else + throw Exception("Unknown hash type " + hashname); +#endif + } + + gnutls_digest_algorithm_t get() const { return hash; } + }; + + class DHParams + { + gnutls_dh_params_t dh_params; + + DHParams() + { + ThrowOnError(gnutls_dh_params_init(&dh_params), "gnutls_dh_params_init() failed"); + } + + public: + /** Import */ + static std::auto_ptr<DHParams> Import(const std::string& dhstr) + { + std::auto_ptr<DHParams> dh(new DHParams); + int ret = gnutls_dh_params_import_pkcs3(dh->dh_params, Datum(dhstr).get(), GNUTLS_X509_FMT_PEM); + ThrowOnError(ret, "Unable to import DH params"); + return dh; + } + + /** Generate */ + static std::auto_ptr<DHParams> Generate(unsigned int bits) + { + std::auto_ptr<DHParams> dh(new DHParams); + ThrowOnError(gnutls_dh_params_generate2(dh->dh_params, bits), "Unable to generate DH params"); + return dh; + } + + ~DHParams() + { + gnutls_dh_params_deinit(dh_params); + } + + const gnutls_dh_params_t& get() const { return dh_params; } + }; + + class X509Key + { + /** Ensure that the key is deinited in case the constructor of X509Key throws + */ + class RAIIKey + { + public: + gnutls_x509_privkey_t key; + + RAIIKey() + { + ThrowOnError(gnutls_x509_privkey_init(&key), "gnutls_x509_privkey_init() failed"); + } + + ~RAIIKey() + { + gnutls_x509_privkey_deinit(key); + } + } key; + + public: + /** Import */ + X509Key(const std::string& keystr) + { + int ret = gnutls_x509_privkey_import(key.key, Datum(keystr).get(), GNUTLS_X509_FMT_PEM); + ThrowOnError(ret, "Unable to import private key"); + } + + gnutls_x509_privkey_t& get() { return key.key; } + }; + + class X509CertList + { + std::vector<gnutls_x509_crt_t> certs; + + public: + /** Import */ + X509CertList(const std::string& certstr) + { + unsigned int certcount = 3; + certs.resize(certcount); + Datum datum(certstr); + + int ret = gnutls_x509_crt_list_import(raw(), &certcount, datum.get(), GNUTLS_X509_FMT_PEM, GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED); + if (ret == GNUTLS_E_SHORT_MEMORY_BUFFER) + { + // the buffer wasn't big enough to hold all certs but gnutls changed certcount to the number of available certs, + // try again with a bigger buffer + certs.resize(certcount); + ret = gnutls_x509_crt_list_import(raw(), &certcount, datum.get(), GNUTLS_X509_FMT_PEM, GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED); + } + + ThrowOnError(ret, "Unable to load certificates"); + + // Resize the vector to the actual number of certs because we rely on its size being correct + // when deallocating the certs + certs.resize(certcount); + } + + ~X509CertList() + { + for (std::vector<gnutls_x509_crt_t>::iterator i = certs.begin(); i != certs.end(); ++i) + gnutls_x509_crt_deinit(*i); + } + + gnutls_x509_crt_t* raw() { return &certs[0]; } + unsigned int size() const { return certs.size(); } + }; + + class X509CRL : public refcountbase + { + class RAIICRL + { + public: + gnutls_x509_crl_t crl; + + RAIICRL() + { + ThrowOnError(gnutls_x509_crl_init(&crl), "gnutls_x509_crl_init() failed"); + } + + ~RAIICRL() + { + gnutls_x509_crl_deinit(crl); + } + } crl; + + public: + /** Import */ + X509CRL(const std::string& crlstr) + { + int ret = gnutls_x509_crl_import(get(), Datum(crlstr).get(), GNUTLS_X509_FMT_PEM); + ThrowOnError(ret, "Unable to load certificate revocation list"); + } + + gnutls_x509_crl_t& get() { return crl.crl; } + }; + +#ifdef GNUTLS_NEW_PRIO_API + class Priority + { + gnutls_priority_t priority; + + public: + Priority(const std::string& priorities) + { + // Try to set the priorities for ciphers, kex methods etc. to the user supplied string + // If the user did not supply anything then the string is already set to "NORMAL" + const char* priocstr = priorities.c_str(); + const char* prioerror; + + int ret = gnutls_priority_init(&priority, priocstr, &prioerror); + if (ret < 0) + { + // gnutls did not understand the user supplied string + throw Exception("Unable to initialize priorities to \"" + priorities + "\": " + gnutls_strerror(ret) + " Syntax error at position " + ConvToStr((unsigned int) (prioerror - priocstr))); + } + } + + ~Priority() + { + gnutls_priority_deinit(priority); + } + + void SetupSession(gnutls_session_t sess) + { + gnutls_priority_set(sess, priority); + } + }; +#else + /** Dummy class, used when gnutls_priority_set() is not available + */ + class Priority + { + public: + Priority(const std::string& priorities) + { + if (priorities != "NORMAL") + throw Exception("You've set a non-default priority string, but GnuTLS lacks support for it"); + } + + static void SetupSession(gnutls_session_t sess) + { + // Always set the default priorities + gnutls_set_default_priority(sess); + } + }; +#endif + + class CertCredentials + { + /** DH parameters associated with these credentials + */ + std::auto_ptr<DHParams> dh; + + protected: + gnutls_certificate_credentials_t cred; + + public: + CertCredentials() + { + ThrowOnError(gnutls_certificate_allocate_credentials(&cred), "Cannot allocate certificate credentials"); + } + + ~CertCredentials() + { + gnutls_certificate_free_credentials(cred); + } + + /** Associates these credentials with the session + */ + void SetupSession(gnutls_session_t sess) + { + gnutls_credentials_set(sess, GNUTLS_CRD_CERTIFICATE, cred); + } + + /** Set the given DH parameters to be used with these credentials + */ + void SetDH(std::auto_ptr<DHParams>& DH) + { + dh = DH; + gnutls_certificate_set_dh_params(cred, dh->get()); + } + }; + + class X509Credentials : public CertCredentials + { + /** Private key + */ + X509Key key; + + /** Certificate list, presented to the peer + */ + X509CertList certs; + + /** Trusted CA, may be NULL + */ + std::auto_ptr<X509CertList> trustedca; + + /** Certificate revocation list, may be NULL + */ + std::auto_ptr<X509CRL> crl; + + static int cert_callback(gnutls_session_t session, const gnutls_datum_t* req_ca_rdn, int nreqs, const gnutls_pk_algorithm_t* sign_algos, int sign_algos_length, cert_cb_last_param_type* st); + + public: + X509Credentials(const std::string& certstr, const std::string& keystr) + : key(keystr) + , certs(certstr) + { + // Throwing is ok here, the destructor of Credentials is called in that case + int ret = gnutls_certificate_set_x509_key(cred, certs.raw(), certs.size(), key.get()); + ThrowOnError(ret, "Unable to set cert/key pair"); + +#ifdef GNUTLS_NEW_CERT_CALLBACK_API + gnutls_certificate_set_retrieve_function(cred, cert_callback); +#else + gnutls_certificate_client_set_retrieve_function(cred, cert_callback); +#endif + } + + /** Sets the trusted CA and the certificate revocation list + * to use when verifying certificates + */ + void SetCA(std::auto_ptr<X509CertList>& certlist, std::auto_ptr<X509CRL>& CRL) + { + // Do nothing if certlist is NULL + if (certlist.get()) + { + int ret = gnutls_certificate_set_x509_trust(cred, certlist->raw(), certlist->size()); + ThrowOnError(ret, "gnutls_certificate_set_x509_trust() failed"); + + if (CRL.get()) + { + ret = gnutls_certificate_set_x509_crl(cred, &CRL->get(), 1); + ThrowOnError(ret, "gnutls_certificate_set_x509_crl() failed"); + } + + trustedca = certlist; + crl = CRL; + } + } + }; + + class Profile : public refcountbase + { + /** Name of this profile + */ + const std::string name; + + /** X509 certificate(s) and key + */ + X509Credentials x509cred; + + /** The minimum length in bits for the DH prime to be accepted as a client + */ + unsigned int min_dh_bits; + + /** Hashing algorithm to use when generating certificate fingerprints + */ + Hash hash; + + /** Priorities for ciphers, compression methods, etc. + */ + Priority priority; + + Profile(const std::string& profilename, const std::string& certstr, const std::string& keystr, + std::auto_ptr<DHParams>& DH, unsigned int mindh, const std::string& hashstr, + const std::string& priostr, std::auto_ptr<X509CertList>& CA, std::auto_ptr<X509CRL>& CRL) + : name(profilename) + , x509cred(certstr, keystr) + , min_dh_bits(mindh) + , hash(hashstr) + , priority(priostr) + { + x509cred.SetDH(DH); + x509cred.SetCA(CA, CRL); + } + + static std::string ReadFile(const std::string& filename) + { + FileReader reader(filename); + std::string ret = reader.GetString(); + if (ret.empty()) + throw Exception("Cannot read file " + filename); + return ret; + } + + public: + static reference<Profile> Create(const std::string& profilename, ConfigTag* tag) + { + std::string certstr = ReadFile(tag->getString("certfile", "cert.pem")); + std::string keystr = ReadFile(tag->getString("keyfile", "key.pem")); + + std::auto_ptr<DHParams> dh; + int gendh = tag->getInt("gendh"); + if (gendh) + { + gendh = (gendh < 1024 ? 1024 : gendh); + dh = DHParams::Generate(gendh); + } + else + dh = DHParams::Import(ReadFile(tag->getString("dhfile", "dhparams.pem"))); + + // Use default priority string if this tag does not specify one + std::string priostr = tag->getString("priority", "NORMAL"); + unsigned int mindh = tag->getInt("mindhbits", 1024); + std::string hashstr = tag->getString("hash", "md5"); + + // Load trusted CA and revocation list, if set + std::auto_ptr<X509CertList> ca; + std::auto_ptr<X509CRL> crl; + std::string filename = tag->getString("cafile"); + if (!filename.empty()) + { + ca.reset(new X509CertList(ReadFile(filename))); + + filename = tag->getString("crlfile"); + if (!filename.empty()) + crl.reset(new X509CRL(ReadFile(filename))); + } + + return new Profile(profilename, certstr, keystr, dh, mindh, hashstr, priostr, ca, crl); + } + + /** Set up the given session with the settings in this profile + */ + void SetupSession(gnutls_session_t sess) + { + priority.SetupSession(sess); + x509cred.SetupSession(sess); + gnutls_dh_set_prime_bits(sess, min_dh_bits); + } + + const std::string& GetName() const { return name; } + X509Credentials& GetX509Credentials() { return x509cred; } + gnutls_digest_algorithm_t GetHash() const { return hash.get(); } + }; +} + /** Represents an SSL user's extra data */ class issl_session @@ -104,6 +552,7 @@ public: gnutls_session_t sess; issl_status status; reference<ssl_cert> cert; + reference<GnuTLS::Profile> profile; issl_session() : socket(NULL), sess(NULL) {} }; @@ -118,13 +567,7 @@ class GnuTLSIOHook : public SSLIOHook gnutls_init(&session->sess, me_server ? GNUTLS_SERVER : GNUTLS_CLIENT); session->socket = user; - #ifdef GNUTLS_NEW_PRIO_API - gnutls_priority_set(session->sess, priority); - #else - gnutls_set_default_priority(session->sess); - #endif - gnutls_credentials_set(session->sess, GNUTLS_CRD_CERTIFICATE, x509_cred); - gnutls_dh_set_prime_bits(session->sess, dh_bits); + session->profile->SetupSession(session->sess); gnutls_transport_set_ptr(session->sess, reinterpret_cast<gnutls_transport_ptr_t>(session)); gnutls_transport_set_push_function(session->sess, gnutls_push_wrapper); gnutls_transport_set_pull_function(session->sess, gnutls_pull_wrapper); @@ -145,6 +588,7 @@ class GnuTLSIOHook : public SSLIOHook session->socket = NULL; session->sess = NULL; session->cert = NULL; + session->profile = NULL; session->status = ISSL_NONE; } @@ -269,7 +713,7 @@ class GnuTLSIOHook : public SSLIOHook gnutls_x509_crt_get_issuer_dn(cert, str, &name_size); certinfo->issuer = str; - if ((ret = gnutls_x509_crt_get_fingerprint(cert, hash, digest, &digest_size)) < 0) + if ((ret = gnutls_x509_crt_get_fingerprint(cert, session->profile->GetHash(), digest, &digest_size)) < 0) { certinfo->error = gnutls_strerror(ret); } @@ -360,13 +804,6 @@ info_done_dealloc: public: issl_session* sessions; - gnutls_certificate_credentials_t x509_cred; - - gnutls_digest_algorithm_t hash; - #ifdef GNUTLS_NEW_PRIO_API - gnutls_priority_t priority; - #endif - int dh_bits; GnuTLSIOHook(Module* parent) : SSLIOHook(parent, "ssl/gnutls") @@ -538,7 +975,7 @@ info_done_dealloc: } }; -static int cert_callback(gnutls_session_t sess, const gnutls_datum_t* req_ca_rdn, int nreqs, const gnutls_pk_algorithm_t* sign_algos, int sign_algos_length, cert_cb_last_param_type* st) +int GnuTLS::X509Credentials::cert_callback(gnutls_session_t sess, const gnutls_datum_t* req_ca_rdn, int nreqs, const gnutls_pk_algorithm_t* sign_algos, int sign_algos_length, cert_cb_last_param_type* st) { #ifndef GNUTLS_NEW_CERT_CALLBACK_API st->type = GNUTLS_CRT_X509; @@ -546,9 +983,12 @@ static int cert_callback(gnutls_session_t sess, const gnutls_datum_t* req_ca_rdn st->cert_type = GNUTLS_CRT_X509; st->key_type = GNUTLS_PRIVKEY_X509; #endif - st->ncerts = x509_certs.size(); - st->cert.x509 = &x509_certs[0]; - st->key.x509 = x509_key; + issl_session* session = reinterpret_cast<issl_session*>(gnutls_transport_get_ptr(sess)); + GnuTLS::X509Credentials& cred = session->profile->GetX509Credentials(); + + st->ncerts = cred.certs.size(); + st->cert.x509 = cred.certs.raw(); + st->key.x509 = cred.key.get(); st->deinit_all = 0; return 0; @@ -556,16 +996,74 @@ static int cert_callback(gnutls_session_t sess, const gnutls_datum_t* req_ca_rdn class ModuleSSLGnuTLS : public Module { - GnuTLSIOHook iohook; + typedef std::vector<reference<GnuTLS::Profile> > ProfileList; - gnutls_dh_params_t dh_params; + // First member of the class, gets constructed first and destructed last + GnuTLS::Init libinit; - std::string sslports; + GnuTLSIOHook iohook; - bool cred_alloc; - bool dh_alloc; + std::string sslports; RandGen randhandler; + ProfileList profiles; + + void ReadProfiles() + { + // First, store all profiles in a new, temporary container. If no problems occur, swap the two + // containers; this way if something goes wrong we can go back and continue using the current profiles, + // avoiding unpleasant situations where no new SSL connections are possible. + ProfileList newprofiles; + + ConfigTagList tags = ServerInstance->Config->ConfTags("sslprofile"); + if (tags.first == tags.second) + { + // No <sslprofile> tags found, create a profile named "gnutls" from settings in the <gnutls> block + const std::string defname = "gnutls"; + ConfigTag* tag = ServerInstance->Config->ConfValue(defname); + ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "No <sslprofile> tags found; using settings from the <gnutls> tag"); + + try + { + reference<GnuTLS::Profile> profile(GnuTLS::Profile::Create(defname, tag)); + newprofiles.push_back(profile); + } + catch (CoreException& ex) + { + throw ModuleException("Error while initializing the default SSL profile - " + ex.GetReason()); + } + } + + for (ConfigIter i = tags.first; i != tags.second; ++i) + { + ConfigTag* tag = i->second; + if (tag->getString("provider") != "gnutls") + continue; + + std::string name = tag->getString("name"); + if (name.empty()) + { + ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Ignoring <sslprofile> tag without name at " + tag->getTagLocation()); + continue; + } + + reference<GnuTLS::Profile> profile; + try + { + profile = GnuTLS::Profile::Create(name, tag); + } + catch (CoreException& ex) + { + throw ModuleException("Error while initializing SSL profile \"" + name + "\" at " + tag->getTagLocation() + " - " + ex.GetReason()); + } + + newprofiles.push_back(profile); + } + + // New profiles are ok, begin using them + // Old profiles are deleted when their refcount drops to zero + profiles.swap(newprofiles); + } public: ModuleSSLGnuTLS() : iohook(this) @@ -573,28 +1071,12 @@ class ModuleSSLGnuTLS : public Module #ifndef GNUTLS_HAS_RND gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); #endif - - gnutls_global_init(); // This must be called once in the program - gnutls_x509_privkey_init(&x509_key); - - #ifdef GNUTLS_NEW_PRIO_API - // Init this here so it's always initialized, avoids an extra boolean - gnutls_priority_init(&iohook.priority, "NORMAL", NULL); - #endif - - cred_alloc = false; - dh_alloc = false; } void init() CXX11_OVERRIDE { - // Needs the flag as it ignores a plain /rehash - OnModuleRehash(NULL,"ssl"); - + ReadProfiles(); ServerInstance->GenRandom = &randhandler; - - // Void return, guess we assume success - gnutls_certificate_set_dh_params(iohook.x509_cred, dh_params); } void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE @@ -641,207 +1123,18 @@ class ModuleSSLGnuTLS : public Module if(param != "ssl") return; - std::string keyfile; - std::string certfile; - std::string cafile; - std::string crlfile; - - ConfigTag* Conf = ServerInstance->Config->ConfValue("gnutls"); - - cafile = ServerInstance->Config->Paths.PrependConfig(Conf->getString("cafile", "ca.pem")); - crlfile = ServerInstance->Config->Paths.PrependConfig(Conf->getString("crlfile", "crl.pem")); - certfile = ServerInstance->Config->Paths.PrependConfig(Conf->getString("certfile", "cert.pem")); - keyfile = ServerInstance->Config->Paths.PrependConfig(Conf->getString("keyfile", "key.pem")); - int dh_bits = Conf->getInt("dhbits"); - std::string hashname = Conf->getString("hash", "md5"); - - // The GnuTLS manual states that the gnutls_set_default_priority() - // call we used previously when initializing the session is the same - // as setting the "NORMAL" priority string. - // Thus if the setting below is not in the config we will behave exactly - // the same as before, when the priority setting wasn't available. - std::string priorities = Conf->getString("priority", "NORMAL"); - - if((dh_bits != 768) && (dh_bits != 1024) && (dh_bits != 2048) && (dh_bits != 3072) && (dh_bits != 4096)) - dh_bits = 1024; - - iohook.dh_bits = dh_bits; - - // As older versions of gnutls can't do this, let's disable it where needed. -#ifdef GNUTLS_HAS_MAC_GET_ID - // As gnutls_digest_algorithm_t and gnutls_mac_algorithm_t are mapped 1:1, we can do this - // There is no gnutls_dig_get_id() at the moment, but it may come later - iohook.hash = (gnutls_digest_algorithm_t)gnutls_mac_get_id(hashname.c_str()); - if (iohook.hash == GNUTLS_DIG_UNKNOWN) - throw ModuleException("Unknown hash type " + hashname); - - // Check if the user is walking around with their head in the ass, - // giving us something that is a valid MAC but not digest - gnutls_hash_hd_t is_digest; - if (gnutls_hash_init(&is_digest, iohook.hash) < 0) - throw ModuleException("Unknown hash type " + hashname); - gnutls_hash_deinit(is_digest, NULL); -#else - if (hashname == "md5") - iohook.hash = GNUTLS_DIG_MD5; - else if (hashname == "sha1") - iohook.hash = GNUTLS_DIG_SHA1; - else - throw ModuleException("Unknown hash type " + hashname); -#endif - - int ret; - - if (dh_alloc) + try { - gnutls_dh_params_deinit(dh_params); - dh_alloc = false; - dh_params = NULL; - } - - if (cred_alloc) - { - // Deallocate the old credentials - gnutls_certificate_free_credentials(iohook.x509_cred); - - for(unsigned int i=0; i < x509_certs.size(); i++) - gnutls_x509_crt_deinit(x509_certs[i]); - x509_certs.clear(); - } - - ret = gnutls_certificate_allocate_credentials(&iohook.x509_cred); - cred_alloc = (ret >= 0); - if (!cred_alloc) - ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Failed to allocate certificate credentials: %s", gnutls_strerror(ret)); - - if((ret =gnutls_certificate_set_x509_trust_file(iohook.x509_cred, cafile.c_str(), GNUTLS_X509_FMT_PEM)) < 0) - ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Failed to set X.509 trust file '%s': %s", cafile.c_str(), gnutls_strerror(ret)); - - if((ret = gnutls_certificate_set_x509_crl_file (iohook.x509_cred, crlfile.c_str(), GNUTLS_X509_FMT_PEM)) < 0) - ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Failed to set X.509 CRL file '%s': %s", crlfile.c_str(), gnutls_strerror(ret)); - - FileReader reader; - - reader.Load(certfile); - std::string cert_string = reader.GetString(); - gnutls_datum_t cert_datum = { (unsigned char*)cert_string.data(), static_cast<unsigned int>(cert_string.length()) }; - - reader.Load(keyfile); - std::string key_string = reader.GetString(); - gnutls_datum_t key_datum = { (unsigned char*)key_string.data(), static_cast<unsigned int>(key_string.length()) }; - - // If this fails, no SSL port will work. At all. So, do the smart thing - throw a ModuleException - unsigned int certcount = 3; - x509_certs.resize(certcount); - ret = gnutls_x509_crt_list_import(&x509_certs[0], &certcount, &cert_datum, GNUTLS_X509_FMT_PEM, GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED); - if (ret == GNUTLS_E_SHORT_MEMORY_BUFFER) - { - // the buffer wasn't big enough to hold all certs but gnutls updated certcount to the number of available certs, try again with a bigger buffer - x509_certs.resize(certcount); - ret = gnutls_x509_crt_list_import(&x509_certs[0], &certcount, &cert_datum, GNUTLS_X509_FMT_PEM, GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED); + ReadProfiles(); } - - if (ret <= 0) + catch (ModuleException& ex) { - // clear the vector so we won't call gnutls_x509_crt_deinit() on the (uninited) certs later - x509_certs.clear(); - throw ModuleException("Unable to load GnuTLS server certificate (" + certfile + "): " + ((ret < 0) ? (std::string(gnutls_strerror(ret))) : "No certs could be read")); - } - x509_certs.resize(ret); - - if((ret = gnutls_x509_privkey_import(x509_key, &key_datum, GNUTLS_X509_FMT_PEM)) < 0) - throw ModuleException("Unable to load GnuTLS server private key (" + keyfile + "): " + std::string(gnutls_strerror(ret))); - - if((ret = gnutls_certificate_set_x509_key(iohook.x509_cred, &x509_certs[0], certcount, x509_key)) < 0) - throw ModuleException("Unable to set GnuTLS cert/key pair: " + std::string(gnutls_strerror(ret))); - - #ifdef GNUTLS_NEW_PRIO_API - // It's safe to call this every time as we cannot have this uninitialized, see constructor and below. - gnutls_priority_deinit(iohook.priority); - - // Try to set the priorities for ciphers, kex methods etc. to the user supplied string - // If the user did not supply anything then the string is already set to "NORMAL" - const char* priocstr = priorities.c_str(); - const char* prioerror; - - if ((ret = gnutls_priority_init(&iohook.priority, priocstr, &prioerror)) < 0) - { - // gnutls did not understand the user supplied string, log and fall back to the default priorities - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Failed to set priorities to \"%s\": %s Syntax error at position %u, falling back to default (NORMAL)", priorities.c_str(), gnutls_strerror(ret), (unsigned int) (prioerror - priocstr)); - gnutls_priority_init(&iohook.priority, "NORMAL", NULL); - } - - #else - if (priorities != "NORMAL") - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "You've set <gnutls:priority> to a value other than the default, but this is only supported with GnuTLS v2.1.7 or newer. Your GnuTLS version is older than that so the option will have no effect."); - #endif - - #if(GNUTLS_VERSION_MAJOR < 2 || ( GNUTLS_VERSION_MAJOR == 2 && GNUTLS_VERSION_MINOR < 12 ) ) - gnutls_certificate_client_set_retrieve_function (iohook.x509_cred, cert_callback); - #else - gnutls_certificate_set_retrieve_function (iohook.x509_cred, cert_callback); - #endif - ret = gnutls_dh_params_init(&dh_params); - dh_alloc = (ret >= 0); - if (!dh_alloc) - { - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Failed to initialise DH parameters: %s", gnutls_strerror(ret)); - return; - } - - std::string dhfile = Conf->getString("dhfile"); - if (!dhfile.empty()) - { - // Try to load DH params from file - reader.Load(dhfile); - std::string dhstring = reader.GetString(); - gnutls_datum_t dh_datum = { (unsigned char*)dhstring.data(), static_cast<unsigned int>(dhstring.length()) }; - - if ((ret = gnutls_dh_params_import_pkcs3(dh_params, &dh_datum, GNUTLS_X509_FMT_PEM)) < 0) - { - // File unreadable or GnuTLS was unhappy with the contents, generate the DH primes now - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Generating DH parameters because I failed to load them from file '%s': %s", dhfile.c_str(), gnutls_strerror(ret)); - GenerateDHParams(); - } - } - else - { - GenerateDHParams(); + ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, ex.GetReason() + " Not applying settings."); } } - void GenerateDHParams() - { - // Generate Diffie Hellman parameters - for use with DHE - // kx algorithms. These should be discarded and regenerated - // once a day, once a week or once a month. Depending on the - // security requirements. - - if (!dh_alloc) - return; - - int ret; - - if((ret = gnutls_dh_params_generate2(dh_params, iohook.dh_bits)) < 0) - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Failed to generate DH parameters (%d bits): %s", iohook.dh_bits, gnutls_strerror(ret)); - } - ~ModuleSSLGnuTLS() { - for(unsigned int i=0; i < x509_certs.size(); i++) - gnutls_x509_crt_deinit(x509_certs[i]); - - gnutls_x509_privkey_deinit(x509_key); - #ifdef GNUTLS_NEW_PRIO_API - gnutls_priority_deinit(iohook.priority); - #endif - - if (dh_alloc) - gnutls_dh_params_deinit(dh_params); - if (cred_alloc) - gnutls_certificate_free_credentials(iohook.x509_cred); - - gnutls_global_deinit(); ServerInstance->GenRandom = &ServerInstance->HandleGenRandom; } @@ -873,10 +1166,18 @@ class ModuleSSLGnuTLS : public Module void OnHookIO(StreamSocket* user, ListenSocket* lsb) CXX11_OVERRIDE { - if (!user->GetIOHook() && lsb->bind_tag->getString("ssl") == "gnutls") + if (!user->GetIOHook()) { - /* Hook the user with our module */ - user->AddIOHook(&iohook); + std::string profilename = lsb->bind_tag->getString("ssl"); + for (ProfileList::const_iterator i = profiles.begin(); i != profiles.end(); ++i) + { + if ((*i)->GetName() == profilename) + { + iohook.sessions[user->GetFd()].profile = *i; + user->AddIOHook(&iohook); + break; + } + } } } diff --git a/src/modules/extra/m_ssl_openssl.cpp b/src/modules/extra/m_ssl_openssl.cpp index 7acbcfc18..11f4a365e 100644 --- a/src/modules/extra/m_ssl_openssl.cpp +++ b/src/modules/extra/m_ssl_openssl.cpp @@ -60,7 +60,180 @@ char* get_error() return ERR_error_string(ERR_get_error(), NULL); } -static int error_callback(const char *str, size_t len, void *u); +static int OnVerify(int preverify_ok, X509_STORE_CTX* ctx); + +namespace OpenSSL +{ + class Exception : public ModuleException + { + public: + Exception(const std::string& reason) + : ModuleException(reason) { } + }; + + class DHParams + { + DH* dh; + + public: + DHParams(const std::string& filename) + { + FILE* dhpfile = fopen(filename.c_str(), "r"); + if (dhpfile == NULL) + throw Exception("Couldn't open DH file " + filename + ": " + strerror(errno)); + + dh = PEM_read_DHparams(dhpfile, NULL, NULL, NULL); + fclose(dhpfile); + if (!dh) + throw Exception("Couldn't read DH params from file " + filename); + } + + ~DHParams() + { + DH_free(dh); + } + + DH* get() + { + return dh; + } + }; + + class Context + { + SSL_CTX* const ctx; + + public: + Context(SSL_CTX* context) + : ctx(context) + { + SSL_CTX_set_mode(ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, OnVerify); + } + + ~Context() + { + SSL_CTX_free(ctx); + } + + bool SetDH(DHParams& dh) + { + return (SSL_CTX_set_tmp_dh(ctx, dh.get()) >= 0); + } + + bool SetCiphers(const std::string& ciphers) + { + return SSL_CTX_set_cipher_list(ctx, ciphers.c_str()); + } + + bool SetCerts(const std::string& filename) + { + return SSL_CTX_use_certificate_chain_file(ctx, filename.c_str()); + } + + bool SetPrivateKey(const std::string& filename) + { + return SSL_CTX_use_PrivateKey_file(ctx, filename.c_str(), SSL_FILETYPE_PEM); + } + + bool SetCA(const std::string& filename) + { + return SSL_CTX_load_verify_locations(ctx, filename.c_str(), 0); + } + + SSL* CreateSession() + { + return SSL_new(ctx); + } + }; + + class Profile : public refcountbase + { + /** Name of this profile + */ + const std::string name; + + /** DH parameters in use + */ + DHParams dh; + + /** OpenSSL makes us have two contexts, one for servers and one for clients + */ + Context ctx; + Context clictx; + + /** Digest to use when generating fingerprints + */ + const EVP_MD* digest; + + /** Last error, set by error_callback() + */ + std::string lasterr; + + static int error_callback(const char* str, size_t len, void* u) + { + Profile* profile = reinterpret_cast<Profile*>(u); + profile->lasterr = std::string(str, len - 1); + return 0; + } + + public: + Profile(const std::string& profilename, ConfigTag* tag) + : name(profilename) + , dh(ServerInstance->Config->Paths.PrependConfig(tag->getString("dhfile", "dh.pem"))) + , ctx(SSL_CTX_new(SSLv23_server_method())) + , clictx(SSL_CTX_new(SSLv23_client_method())) + { + if ((!ctx.SetDH(dh)) || (!clictx.SetDH(dh))) + throw Exception("Couldn't set DH parameters"); + + std::string hash = tag->getString("hash", "md5"); + digest = EVP_get_digestbyname(hash.c_str()); + if (digest == NULL) + throw Exception("Unknown hash type " + hash); + + std::string ciphers = tag->getString("ciphers"); + if (!ciphers.empty()) + { + if ((!ctx.SetCiphers(ciphers)) || (!clictx.SetCiphers(ciphers))) + { + ERR_print_errors_cb(error_callback, this); + throw Exception("Can't set cipher list to \"" + ciphers + "\" " + lasterr); + } + } + + /* Load our keys and certificates + * NOTE: OpenSSL's error logging API sucks, don't blame us for this clusterfuck. + */ + std::string filename = ServerInstance->Config->Paths.PrependConfig(tag->getString("certfile", "cert.pem")); + if ((!ctx.SetCerts(filename)) || (!clictx.SetCerts(filename))) + { + ERR_print_errors_cb(error_callback, this); + throw Exception("Can't read certificate file: " + lasterr); + } + + filename = ServerInstance->Config->Paths.PrependConfig(tag->getString("keyfile", "key.pem")); + if ((!ctx.SetPrivateKey(filename)) || (!clictx.SetPrivateKey(filename))) + { + ERR_print_errors_cb(error_callback, this); + throw Exception("Can't read key file: " + lasterr); + } + + // Load the CAs we trust + filename = ServerInstance->Config->Paths.PrependConfig(tag->getString("cafile", "ca.pem")); + if ((!ctx.SetCA(filename)) || (!clictx.SetCA(filename))) + { + ERR_print_errors_cb(error_callback, this); + ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Can't read CA list from %s. This is only a problem if you want to verify client certificates, otherwise it's safe to ignore this message. Error: %s", filename.c_str(), lasterr.c_str()); + } + } + + const std::string& GetName() const { return name; } + SSL* CreateServerSession() { return ctx.CreateSession(); } + SSL* CreateClientSession() { return clictx.CreateSession(); } + const EVP_MD* GetDigest() { return digest; } + }; +} /** Represents an SSL user's extra data */ @@ -70,6 +243,7 @@ public: SSL* sess; issl_status status; reference<ssl_cert> cert; + reference<OpenSSL::Profile> profile; bool outbound; bool data_to_write; @@ -198,7 +372,7 @@ class OpenSSLIOHook : public SSLIOHook certinfo->dn = X509_NAME_oneline(X509_get_subject_name(cert),0,0); certinfo->issuer = X509_NAME_oneline(X509_get_issuer_name(cert),0,0); - if (!X509_digest(cert, digest, md, &n)) + if (!X509_digest(cert, session->profile->GetDigest(), md, &n)) { certinfo->error = "Out of memory generating fingerprint"; } @@ -217,9 +391,6 @@ class OpenSSLIOHook : public SSLIOHook public: issl_session* sessions; - SSL_CTX* ctx; - SSL_CTX* clictx; - const EVP_MD *digest; OpenSSLIOHook(Module* mod) : SSLIOHook(mod, "ssl/openssl") @@ -238,7 +409,7 @@ class OpenSSLIOHook : public SSLIOHook issl_session* session = &sessions[fd]; - session->sess = SSL_new(ctx); + session->sess = session->profile->CreateServerSession(); session->status = ISSL_NONE; session->outbound = false; session->cert = NULL; @@ -264,7 +435,7 @@ class OpenSSLIOHook : public SSLIOHook issl_session* session = &sessions[fd]; - session->sess = SSL_new(clictx); + session->sess = session->profile->CreateClientSession(); session->status = ISSL_NONE; session->outbound = true; @@ -457,47 +628,91 @@ class OpenSSLIOHook : public SSLIOHook class ModuleSSLOpenSSL : public Module { + typedef std::vector<reference<OpenSSL::Profile> > ProfileList; + std::string sslports; OpenSSLIOHook iohook; + ProfileList profiles; - public: - ModuleSSLOpenSSL() : iohook(this) + void ReadProfiles() { - /* Global SSL library initialization*/ - SSL_library_init(); - SSL_load_error_strings(); + ProfileList newprofiles; + ConfigTagList tags = ServerInstance->Config->ConfTags("sslprofile"); + if (tags.first == tags.second) + { + // Create a default profile named "openssl" + const std::string defname = "openssl"; + ConfigTag* tag = ServerInstance->Config->ConfValue(defname); + ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "No <sslprofile> tags found, using settings from the <openssl> tag"); - /* Build our SSL contexts: - * NOTE: OpenSSL makes us have two contexts, one for servers and one for clients. ICK. - */ - iohook.ctx = SSL_CTX_new( SSLv23_server_method() ); - iohook.clictx = SSL_CTX_new( SSLv23_client_method() ); + try + { + reference<OpenSSL::Profile> profile(new OpenSSL::Profile(defname, tag)); + newprofiles.push_back(profile); + } + catch (OpenSSL::Exception& ex) + { + throw ModuleException("Error while initializing the default SSL profile - " + ex.GetReason()); + } + } + + for (ConfigIter i = tags.first; i != tags.second; ++i) + { + ConfigTag* tag = i->second; + if (tag->getString("provider") != "openssl") + continue; - SSL_CTX_set_mode(iohook.ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); - SSL_CTX_set_mode(iohook.clictx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + std::string name = tag->getString("name"); + if (name.empty()) + { + ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Ignoring <sslprofile> tag without name at " + tag->getTagLocation()); + continue; + } - SSL_CTX_set_verify(iohook.ctx, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, OnVerify); - SSL_CTX_set_verify(iohook.clictx, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, OnVerify); + reference<OpenSSL::Profile> profile; + try + { + profile = new OpenSSL::Profile(name, tag); + } + catch (CoreException& ex) + { + throw ModuleException("Error while initializing SSL profile \"" + name + "\" at " + tag->getTagLocation() + " - " + ex.GetReason()); + } + + newprofiles.push_back(profile); + } + + profiles.swap(newprofiles); } - ~ModuleSSLOpenSSL() + public: + ModuleSSLOpenSSL() : iohook(this) { - SSL_CTX_free(iohook.ctx); - SSL_CTX_free(iohook.clictx); + // Initialize OpenSSL + SSL_library_init(); + SSL_load_error_strings(); } void init() CXX11_OVERRIDE { - // Needs the flag as it ignores a plain /rehash - OnModuleRehash(NULL,"ssl"); + ReadProfiles(); } void OnHookIO(StreamSocket* user, ListenSocket* lsb) CXX11_OVERRIDE { - if (!user->GetIOHook() && lsb->bind_tag->getString("ssl") == "openssl") + if (user->GetIOHook()) + return; + + ConfigTag* tag = lsb->bind_tag; + std::string profilename = tag->getString("ssl"); + for (ProfileList::const_iterator i = profiles.begin(); i != profiles.end(); ++i) { - /* Hook the user with our module */ - user->AddIOHook(&iohook); + if ((*i)->GetName() == profilename) + { + iohook.sessions[user->GetFd()].profile = *i; + user->AddIOHook(&iohook); + break; + } } } @@ -545,78 +760,14 @@ class ModuleSSLOpenSSL : public Module if (param != "ssl") return; - std::string keyfile; - std::string certfile; - std::string cafile; - std::string dhfile; - - ConfigTag* conf = ServerInstance->Config->ConfValue("openssl"); - - cafile = ServerInstance->Config->Paths.PrependConfig(conf->getString("cafile", "ca.pem")); - certfile = ServerInstance->Config->Paths.PrependConfig(conf->getString("certfile", "cert.pem")); - keyfile = ServerInstance->Config->Paths.PrependConfig(conf->getString("keyfile", "key.pem")); - dhfile = ServerInstance->Config->Paths.PrependConfig(conf->getString("dhfile", "dhparams.pem")); - std::string hash = conf->getString("hash", "md5"); - - iohook.digest = EVP_get_digestbyname(hash.c_str()); - if (iohook.digest == NULL) - throw ModuleException("Unknown hash type " + hash); - - std::string ciphers = conf->getString("ciphers", ""); - - SSL_CTX* ctx = iohook.ctx; - SSL_CTX* clictx = iohook.clictx; - - if (!ciphers.empty()) - { - if ((!SSL_CTX_set_cipher_list(ctx, ciphers.c_str())) || (!SSL_CTX_set_cipher_list(clictx, ciphers.c_str()))) - { - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Can't set cipher list to %s.", ciphers.c_str()); - ERR_print_errors_cb(error_callback, this); - } - } - - /* Load our keys and certificates - * NOTE: OpenSSL's error logging API sucks, don't blame us for this clusterfuck. - */ - if ((!SSL_CTX_use_certificate_chain_file(ctx, certfile.c_str())) || (!SSL_CTX_use_certificate_chain_file(clictx, certfile.c_str()))) - { - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Can't read certificate file %s. %s", certfile.c_str(), strerror(errno)); - ERR_print_errors_cb(error_callback, this); - } - - if (((!SSL_CTX_use_PrivateKey_file(ctx, keyfile.c_str(), SSL_FILETYPE_PEM))) || (!SSL_CTX_use_PrivateKey_file(clictx, keyfile.c_str(), SSL_FILETYPE_PEM))) + try { - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Can't read key file %s. %s", keyfile.c_str(), strerror(errno)); - ERR_print_errors_cb(error_callback, this); + ReadProfiles(); } - - /* Load the CAs we trust*/ - if (((!SSL_CTX_load_verify_locations(ctx, cafile.c_str(), 0))) || (!SSL_CTX_load_verify_locations(clictx, cafile.c_str(), 0))) + catch (ModuleException& ex) { - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Can't read CA list from %s. This is only a problem if you want to verify client certificates, otherwise it's safe to ignore this message. Error: %s", cafile.c_str(), strerror(errno)); - ERR_print_errors_cb(error_callback, this); + ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, ex.GetReason() + " Not applying settings."); } - - FILE* dhpfile = fopen(dhfile.c_str(), "r"); - DH* ret; - - if (dhpfile == NULL) - { - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Couldn't open DH file %s: %s", dhfile.c_str(), strerror(errno)); - throw ModuleException("Couldn't open DH file " + dhfile + ": " + strerror(errno)); - } - else - { - ret = PEM_read_DHparams(dhpfile, NULL, NULL, NULL); - if ((SSL_CTX_set_tmp_dh(ctx, ret) < 0) || (SSL_CTX_set_tmp_dh(clictx, ret) < 0)) - { - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Couldn't set DH parameters %s. SSL errors follow:", dhfile.c_str()); - ERR_print_errors_cb(error_callback, this); - } - } - - fclose(dhpfile); } void On005Numeric(std::map<std::string, std::string>& tokens) CXX11_OVERRIDE @@ -652,20 +803,4 @@ class ModuleSSLOpenSSL : public Module } }; -static int error_callback(const char *str, size_t len, void *u) -{ - ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "SSL error: " + std::string(str, len - 1)); - - // - // XXX: Remove this line, it causes valgrind warnings... - // - // MD_update(&m, buf, j); - // - // - // ... ONLY JOKING! :-) - // - - return 0; -} - MODULE_INIT(ModuleSSLOpenSSL) |