+ 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;
+#ifdef INSPIRCD_GNUTLS_ENABLE_SHA256_FINGERPRINT
+ else if (hashname == "sha256")
+ hash = GNUTLS_DIG_SHA256;
+#endif
+ 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;
+ }
+
+ ~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);
+ }
+
+ static const char* GetDefault()
+ {
+ return "NORMAL:%SERVER_PRECEDENCE:-VERS-SSL3.0";
+ }
+
+ static std::string RemoveUnknownTokens(const std::string& prio)
+ {
+ std::string ret;
+ irc::sepstream ss(prio, ':');
+ for (std::string token; ss.GetToken(token); )
+ {
+ // Save current position so we can revert later if needed
+ const std::string::size_type prevpos = ret.length();
+ // Append next token
+ if (!ret.empty())
+ ret.push_back(':');
+ ret.append(token);
+
+ gnutls_priority_t test;
+ if (gnutls_priority_init(&test, ret.c_str(), NULL) < 0)
+ {
+ // The new token broke the priority string, revert to the previously working one
+ ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Priority string token not recognized: \"%s\"", token.c_str());
+ ret.erase(prevpos);
+ }
+ else
+ {
+ // Worked
+ gnutls_priority_deinit(test);
+ }
+ }
+ return ret;
+ }
+ };
+#else
+ /** Dummy class, used when gnutls_priority_set() is not available
+ */
+ class Priority
+ {
+ public:
+ Priority(const std::string& priorities)
+ {
+ if (priorities != GetDefault())
+ 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);
+ }
+
+ static const char* GetDefault()
+ {
+ return "NORMAL";
+ }
+
+ static std::string RemoveUnknownTokens(const std::string& prio)
+ {
+ // We don't do anything here because only NORMAL is accepted
+ return prio;
+ }
+ };
+#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 DataReader
+ {
+ int retval;
+#ifdef INSPIRCD_GNUTLS_HAS_RECV_PACKET
+ gnutls_packet_t packet;
+
+ public:
+ DataReader(gnutls_session_t sess)
+ {
+ // Using the packet API avoids the final copy of the data which GnuTLS does if we supply
+ // our own buffer. Instead, we get the buffer containing the data from GnuTLS and copy it
+ // to the recvq directly from there in appendto().
+ retval = gnutls_record_recv_packet(sess, &packet);
+ }
+
+ void appendto(std::string& recvq)
+ {
+ // Copy data from GnuTLS buffers to recvq
+ gnutls_datum_t datum;
+ gnutls_packet_get(packet, &datum, NULL);
+ recvq.append(reinterpret_cast<const char*>(datum.data), datum.size);
+
+ gnutls_packet_deinit(packet);
+ }
+#else
+ char* const buffer;
+
+ public:
+ DataReader(gnutls_session_t sess)
+ : buffer(ServerInstance->GetReadBuffer())
+ {
+ // Read data from GnuTLS buffers into ReadBuffer
+ retval = gnutls_record_recv(sess, buffer, ServerInstance->Config->NetBufferSize);
+ }
+
+ void appendto(std::string& recvq)
+ {
+ // Copy data from ReadBuffer to recvq
+ recvq.append(buffer, retval);
+ }
+#endif
+
+ int ret() const { return retval; }
+ };
+
+ 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;
+
+ /** Rough max size of records to send
+ */
+ const unsigned int outrecsize;
+
+ 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,
+ unsigned int recsize)
+ : name(profilename)
+ , x509cred(certstr, keystr)
+ , min_dh_bits(mindh)
+ , hash(hashstr)
+ , priority(priostr)
+ , outrecsize(recsize)
+ {
+ 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;
+ }
+
+ static std::string GetPrioStr(const std::string& profilename, ConfigTag* tag)
+ {
+ // Use default priority string if this tag does not specify one
+ std::string priostr = GnuTLS::Priority::GetDefault();
+ bool found = tag->readString("priority", priostr);
+ // If the prio string isn't set in the config don't be strict about the default one because it doesn't work on all versions of GnuTLS
+ if (!tag->getBool("strictpriority", found))
+ {
+ std::string stripped = GnuTLS::Priority::RemoveUnknownTokens(priostr);
+ if (stripped.empty())
+ {
+ // Stripping failed, act as if a prio string wasn't set
+ stripped = GnuTLS::Priority::RemoveUnknownTokens(GnuTLS::Priority::GetDefault());
+ ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Priority string for profile \"%s\" contains unknown tokens and stripping it didn't yield a working one either, falling back to \"%s\"", profilename.c_str(), stripped.c_str());
+ }
+ else if ((found) && (stripped != priostr))
+ {
+ // Prio string was set in the config and we ended up with something that works but different
+ ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Priority string for profile \"%s\" contains unknown tokens, stripped to \"%s\"", profilename.c_str(), stripped.c_str());
+ }
+ priostr.swap(stripped);
+ }
+ return priostr;
+ }
+
+ 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 = DHParams::Import(ReadFile(tag->getString("dhfile", "dhparams.pem")));
+
+ std::string priostr = GetPrioStr(profilename, tag);
+ 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)));
+ }
+
+#ifdef INSPIRCD_GNUTLS_HAS_CORK
+ // If cork support is available outrecsize represents the (rough) max amount of data we give GnuTLS while corked
+ unsigned int outrecsize = tag->getInt("outrecsize", 2048, 512);
+#else
+ unsigned int outrecsize = tag->getInt("outrecsize", 2048, 512, 16384);
+#endif
+ return new Profile(profilename, certstr, keystr, dh, mindh, hashstr, priostr, ca, crl, outrecsize);
+ }
+
+ /** 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);
+
+ // Request client certificate if we are a server, no-op if we're a client
+ gnutls_certificate_server_set_request(sess, GNUTLS_CERT_REQUEST);
+ }
+
+ const std::string& GetName() const { return name; }
+ X509Credentials& GetX509Credentials() { return x509cred; }
+ gnutls_digest_algorithm_t GetHash() const { return hash.get(); }
+ unsigned int GetOutgoingRecordSize() const { return outrecsize; }
+ };
+}
+
+class GnuTLSIOHook : public SSLIOHook
+{
+ private:
+ gnutls_session_t sess;
+ issl_status status;
+ reference<GnuTLS::Profile> profile;
+#ifdef INSPIRCD_GNUTLS_HAS_CORK
+ size_t gbuffersize;
+#endif