diff options
Diffstat (limited to 'modules/nickserv/ns_cert.cpp')
-rw-r--r-- | modules/nickserv/ns_cert.cpp | 455 |
1 files changed, 455 insertions, 0 deletions
diff --git a/modules/nickserv/ns_cert.cpp b/modules/nickserv/ns_cert.cpp new file mode 100644 index 000000000..5594b2e57 --- /dev/null +++ b/modules/nickserv/ns_cert.cpp @@ -0,0 +1,455 @@ +/* NickServ core functions + * + * (C) 2003-2024 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + +#include "module.h" +#include "modules/ns_cert.h" + +static Anope::unordered_map<NickCore *> certmap; + +struct CertServiceImpl final + : CertService +{ + CertServiceImpl(Module *o) : CertService(o) { } + + NickCore *FindAccountFromCert(const Anope::string &cert) override + { + Anope::unordered_map<NickCore *>::iterator it = certmap.find(cert); + if (it != certmap.end()) + return it->second; + return NULL; + } + + void ReplaceCert(const Anope::string &oldcert, const Anope::string &newcert) override + { + auto *nc = FindAccountFromCert(oldcert); + if (!nc) + return; + + auto *cl = nc->GetExt<NSCertList>("certificates"); + if (cl) + cl->ReplaceCert(oldcert, newcert); + } +}; + +struct NSCertListImpl final + : NSCertList +{ + Serialize::Reference<NickCore> nc; + std::vector<Anope::string> certs; + +public: + NSCertListImpl(Extensible *obj) : nc(anope_dynamic_static_cast<NickCore *>(obj)) { } + + ~NSCertListImpl() override + { + ClearCert(); + } + + /** Add an entry to the nick's certificate list + * + * @param entry The fingerprint to add to the cert list + * + * Adds a new entry into the cert list. + */ + void AddCert(const Anope::string &entry) override + { + this->certs.push_back(entry); + certmap[entry] = nc; + FOREACH_MOD(OnNickAddCert, (this->nc, entry)); + } + + /** Get an entry from the nick's cert list by index + * + * @param entry Index in the certificate list vector to retrieve + * @return The fingerprint entry of the given index if within bounds, an empty string if the vector is empty or the index is out of bounds + * + * Retrieves an entry from the certificate list corresponding to the given index. + */ + Anope::string GetCert(unsigned entry) const override + { + if (entry >= this->certs.size()) + return ""; + return this->certs[entry]; + } + + unsigned GetCertCount() const override + { + return this->certs.size(); + } + + /** Find an entry in the nick's cert list + * + * @param entry The fingerprint to search for + * @return True if the fingerprint is found in the cert list, false otherwise + * + * Search for an fingerprint within the cert list. + */ + bool FindCert(const Anope::string &entry) const override + { + return std::find(this->certs.begin(), this->certs.end(), entry) != this->certs.end(); + } + + /** Erase a fingerprint from the nick's certificate list + * + * @param entry The fingerprint to remove + * + * Removes the specified fingerprint from the cert list. + */ + void EraseCert(const Anope::string &entry) override + { + std::vector<Anope::string>::iterator it = std::find(this->certs.begin(), this->certs.end(), entry); + if (it != this->certs.end()) + { + FOREACH_MOD(OnNickEraseCert, (this->nc, entry)); + certmap.erase(entry); + this->certs.erase(it); + } + } + + void ReplaceCert(const Anope::string &oldentry, const Anope::string &newentry) override + { + auto it = std::find(this->certs.begin(), this->certs.end(), oldentry); + if (it == this->certs.end()) + return; // We can't replace a non-existent cert. + + FOREACH_MOD(OnNickEraseCert, (this->nc, oldentry)); + certmap.erase(oldentry); + + if (std::find(this->certs.begin(), this->certs.end(), newentry) != this->certs.end()) + { + // The cert we're upgrading to already exists. + this->certs.erase(it); + return; + } + + *it = newentry; + certmap[newentry] = nc; + FOREACH_MOD(OnNickAddCert, (this->nc, newentry)); + } + + /** Clears the entire nick's cert list + * + * Deletes all the memory allocated in the certificate list vector and then clears the vector. + */ + void ClearCert() override + { + FOREACH_MOD(OnNickClearCert, (this->nc)); + for (const auto &cert : certs) + certmap.erase(cert); + this->certs.clear(); + } + + void Check() override + { + if (this->certs.empty()) + nc->Shrink<NSCertList>("certificates"); + } + + struct ExtensibleItem final + : ::ExtensibleItem<NSCertListImpl> + { + ExtensibleItem(Module *m, const Anope::string &ename) : ::ExtensibleItem<NSCertListImpl>(m, ename) { } + + void ExtensibleSerialize(const Extensible *e, const Serializable *s, Serialize::Data &data) const override + { + if (s->GetSerializableType()->GetName() != "NickCore") + return; + + const NickCore *n = anope_dynamic_static_cast<const NickCore *>(e); + NSCertList *c = this->Get(n); + if (c == NULL || !c->GetCertCount()) + return; + + std::ostringstream oss; + for (unsigned i = 0; i < c->GetCertCount(); ++i) + oss << c->GetCert(i) << " "; + data.Store("cert", oss.str()); + } + + void ExtensibleUnserialize(Extensible *e, Serializable *s, Serialize::Data &data) override + { + if (s->GetSerializableType()->GetName() != "NickCore") + return; + + NickCore *n = anope_dynamic_static_cast<NickCore *>(e); + NSCertListImpl *c = this->Require(n); + + Anope::string buf; + data["cert"] >> buf; + spacesepstream sep(buf); + for (const auto &cert : c->certs) + certmap.erase(cert); + c->certs.clear(); + while (sep.GetToken(buf)) + { + c->certs.push_back(buf); + certmap[buf] = n; + } + } + }; +}; + +class CommandNSCert final + : public Command +{ +private: + void DoAdd(CommandSource &source, NickCore *nc, Anope::string certfp) + { + NSCertList *cl = nc->Require<NSCertList>("certificates"); + unsigned max = Config->GetModule(this->owner)->Get<unsigned>("max", "5"); + + if (cl->GetCertCount() >= max) + { + source.Reply(_("Sorry, the maximum of %d certificate entries has been reached."), max); + return; + } + + if (source.GetAccount() == nc) + { + User *u = source.GetUser(); + + if (!u || u->fingerprint.empty()) + { + source.Reply(_("You are not using a client certificate.")); + return; + } + + certfp = u->fingerprint; + } + + if (cl->FindCert(certfp)) + { + source.Reply(_("Fingerprint \002%s\002 already present on %s's certificate list."), certfp.c_str(), nc->display.c_str()); + return; + } + + if (certmap.find(certfp) != certmap.end()) + { + source.Reply(_("Fingerprint \002%s\002 is already in use."), certfp.c_str()); + return; + } + + cl->AddCert(certfp); + Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to ADD certificate fingerprint " << certfp << " to " << nc->display; + source.Reply(_("\002%s\002 added to %s's certificate list."), certfp.c_str(), nc->display.c_str()); + } + + void DoDel(CommandSource &source, NickCore *nc, Anope::string certfp) + { + NSCertList *cl = nc->Require<NSCertList>("certificates"); + + if (certfp.empty()) + { + User *u = source.GetUser(); + if (u) + certfp = u->fingerprint; + } + + if (certfp.empty()) + { + this->OnSyntaxError(source, "DEL"); + return; + } + + if (!cl->FindCert(certfp)) + { + source.Reply(_("\002%s\002 not found on %s's certificate list."), certfp.c_str(), nc->display.c_str()); + return; + } + + cl->EraseCert(certfp); + cl->Check(); + Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to DELETE certificate fingerprint " << certfp << " from " << nc->display; + source.Reply(_("\002%s\002 deleted from %s's certificate list."), certfp.c_str(), nc->display.c_str()); + } + + static void DoList(CommandSource &source, const NickCore *nc) + { + NSCertList *cl = nc->GetExt<NSCertList>("certificates"); + + if (!cl || !cl->GetCertCount()) + { + source.Reply(_("%s's certificate list is empty."), nc->display.c_str()); + return; + } + + source.Reply(_("Certificate list for %s:"), nc->display.c_str()); + for (unsigned i = 0; i < cl->GetCertCount(); ++i) + { + Anope::string fingerprint = cl->GetCert(i); + source.Reply(" %s", fingerprint.c_str()); + } + } + +public: + CommandNSCert(Module *creator) : Command(creator, "nickserv/cert", 1, 3) + { + this->SetDesc(_("Modify the nickname client certificate list")); + this->SetSyntax(_("ADD [\037nickname\037] [\037fingerprint\037]")); + this->SetSyntax(_("DEL [\037nickname\037] \037fingerprint\037")); + this->SetSyntax(_("LIST [\037nickname\037]")); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) override + { + const Anope::string &cmd = params[0]; + Anope::string nick, certfp; + + if (cmd.equals_ci("LIST")) + nick = params.size() > 1 ? params[1] : ""; + else + { + nick = params.size() == 3 ? params[1] : ""; + certfp = params.size() > 1 ? params[params.size() - 1] : ""; + } + + NickCore *nc; + if (!nick.empty()) + { + const NickAlias *na = NickAlias::Find(nick); + if (na == NULL) + { + source.Reply(NICK_X_NOT_REGISTERED, nick.c_str()); + return; + } + else if (na->nc != source.GetAccount() && !source.HasPriv("nickserv/cert")) + { + source.Reply(ACCESS_DENIED); + return; + } + else if (Config->GetModule("nickserv")->Get<bool>("secureadmins", "yes") && source.GetAccount() != na->nc && na->nc->IsServicesOper() && !cmd.equals_ci("LIST")) + { + source.Reply(_("You may view but not modify the certificate list of other Services Operators.")); + return; + } + + nc = na->nc; + } + else + nc = source.nc; + + if (cmd.equals_ci("LIST")) + return this->DoList(source, nc); + else if (nc->HasExt("NS_SUSPENDED")) + source.Reply(NICK_X_SUSPENDED, nc->display.c_str()); + else if (Anope::ReadOnly) + source.Reply(READ_ONLY_MODE); + else if (cmd.equals_ci("ADD")) + return this->DoAdd(source, nc, certfp); + else if (cmd.equals_ci("DEL")) + return this->DoDel(source, nc, certfp); + else + this->OnSyntaxError(source, ""); + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Modifies or displays the certificate list for your nick.\n" + "If you connect to IRC and provide a client certificate with a\n" + "matching fingerprint in the cert list, you will be\n" + "automatically identified to services. Services Operators\n" + "may provide a nick to modify other users' certificate lists.\n" + " \n")); + source.Reply(_("Examples:\n" + " \n" + " \002CERT ADD\002\n" + " Adds your current fingerprint to the certificate list and\n" + " automatically identifies you when you connect to IRC\n" + " using this fingerprint.\n" + " \n" + " \002CERT DEL <fingerprint>\002\n" + " Removes the fingerprint <fingerprint> from your certificate list.\n" + " \n" + " \002CERT LIST\002\n" + " Displays the current certificate list.")); + return true; + } +}; + +class NSCert final + : public Module +{ + CommandNSCert commandnscert; + NSCertListImpl::ExtensibleItem certs; + CertServiceImpl cs; + +public: + NSCert(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), + commandnscert(this), certs(this, "certificates"), cs(this) + { + if (!IRCD || !IRCD->CanCertFP) + throw ModuleException("Your IRCd does not support ssl client certificates"); + } + + void OnFingerprint(User *u) override + { + BotInfo *NickServ = Config->GetClient("NickServ"); + if (!NickServ || u->IsIdentified()) + return; + + NickCore *nc = cs.FindAccountFromCert(u->fingerprint); + if (!nc || nc->HasExt("NS_SUSPENDED")) + return; + + unsigned int maxlogins = Config->GetModule("ns_identify")->Get<unsigned int>("maxlogins"); + if (maxlogins && nc->users.size() >= maxlogins) + { + u->SendMessage(NickServ, _("Account \002%s\002 has already reached the maximum number of simultaneous logins (%u)."), nc->display.c_str(), maxlogins); + return; + } + + NickAlias *na = NickAlias::Find(u->nick); + if (na && na->nc == nc) + u->Identify(na); + else + u->Login(nc); + + u->SendMessage(NickServ, _("SSL certificate fingerprint accepted, you are now identified to \002%s\002."), nc->display.c_str()); + Log(NickServ) << u->GetMask() << " automatically identified for account " << nc->display << " via SSL certificate fingerprint"; + } + + void OnNickRegister(User *u, NickAlias *na, const Anope::string &pass) override + { + if (!Config->GetModule(this)->Get<bool>("automatic", "yes") || !u || u->fingerprint.empty()) + return; + + auto *cl = certs.Require(na->nc); + cl->AddCert(u->fingerprint); + } + + EventReturn OnNickValidate(User *u, NickAlias *na) override + { + NSCertList *cl = certs.Get(na->nc); + if (!u->fingerprint.empty() && cl && cl->FindCert(u->fingerprint)) + { + BotInfo *NickServ = Config->GetClient("NickServ"); + + unsigned int maxlogins = Config->GetModule("ns_identify")->Get<unsigned int>("maxlogins"); + if (maxlogins && na->nc->users.size() >= maxlogins) + { + u->SendMessage(NickServ, _("Account \002%s\002 has already reached the maximum number of simultaneous logins (%u)."), na->nc->display.c_str(), maxlogins); + return EVENT_CONTINUE; + } + + u->Identify(na); + + u->SendMessage(NickServ, _("SSL certificate fingerprint accepted, you are now identified.")); + Log(NickServ) << u->GetMask() << " automatically identified for account " << na->nc->display << " via SSL certificate fingerprint"; + return EVENT_ALLOW; + } + + return EVENT_CONTINUE; + } +}; + +MODULE_INIT(NSCert) |