diff options
-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) |