/* * InspIRCd -- Internet Relay Chat Daemon * * Copyright (C) 2021 Herman * Copyright (C) 2016 Adam * Copyright (C) 2014 Mantas Mikulėnas * Copyright (C) 2013, 2017-2024 Sadie Powell * Copyright (C) 2013, 2015-2016, 2018 Attila Molnar * Copyright (C) 2013 Daniel Vassdal * Copyright (C) 2012 Robby * Copyright (C) 2009-2010 Daniel De Graaf * Copyright (C) 2008 Thomas Stagner * Copyright (C) 2008 Craig Edwards * * This file is part of InspIRCd. InspIRCd is free software: you can * redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 2. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "inspircd.h" #include "modules/account.h" #include "modules/cap.h" #include "modules/server.h" #include "modules/ssl.h" enum { // From IRCv3 sasl-3.1 RPL_SASLSUCCESS = 903, ERR_SASLFAIL = 904, ERR_SASLTOOLONG = 905, ERR_SASLABORTED = 906, RPL_SASLMECHS = 908 }; namespace { Account::ProviderAPI* g_accountproviderapi; ClientProtocol::EventProvider* g_protoev; void SendSASL(LocalUser* user, const std::string& agent, char mode, const std::vector& parameters) { auto* target = (*g_accountproviderapi)->GetServer(); if (!target) return; // Should never happen. CommandBase::Params params; params.push_back(user->uuid); params.push_back(agent); params.push_back(ConvToStr(mode)); params.insert(params.end(), parameters.begin(), parameters.end()); ServerInstance->PI->SendEncapsulatedData(target->GetName(), "SASL", params); } } class SASLCap final : public Cap::Capability { private: std::string mechlist; bool OnRequest(LocalUser* user, bool adding) override { // Servers MUST NAK any sasl capability request if the authentication layer // is unavailable. return OnList(user); } bool OnList(LocalUser* user) override { // Servers MUST NOT advertise the sasl capability if the authentication layer // is unavailable. return *g_accountproviderapi; } const std::string* GetValue(LocalUser* user) const override { return &mechlist; } public: SASLCap(const WeakModulePtr& mod) : Cap::Capability(mod, "sasl") { } void SetMechlist(const std::string& newmechlist) { if (mechlist == newmechlist) return; // No change. mechlist = newmechlist; NotifyValueChange(); } }; enum SaslState { SASL_INIT, SASL_COMM, SASL_DONE }; enum SaslResult { SASL_OK, SASL_FAIL, SASL_ABORT }; /** * Tracks SASL authentication state like charybdis does. --nenolod */ class SaslAuthenticator final { private: std::string agent; LocalUser* user; SaslState state = SASL_INIT; SaslResult result; bool state_announced = false; void SendHostIP(UserCertificateAPI& sslapi) { std::vector params; params.reserve(3); params.push_back(user->GetRealHost()); params.push_back(user->GetAddress()); params.emplace_back(sslapi && sslapi->IsSecure(user) ? "S" : "P"); SendSASL(user, "*", 'H', params); } public: SaslAuthenticator(LocalUser* lu, const std::string& method, UserCertificateAPI& sslapi) : user(lu) { SendHostIP(sslapi); std::vector params; params.push_back(method); if (sslapi) { for (const auto& fingerprint : sslapi->GetFingerprints(user)) params.push_back(fingerprint); } SendSASL(user, "*", 'S', params); } static SaslResult GetSaslResult(const std::string& result_) { if (result_ == "F") return SASL_FAIL; if (result_ == "A") return SASL_ABORT; return SASL_OK; } /* checks for and deals with a state change. */ SaslState ProcessInboundMessage(const CommandBase::Params& msg) { switch (this->state) { case SASL_INIT: this->agent = msg[0]; this->state = SASL_COMM; [[fallthrough]]; case SASL_COMM: if (msg[0] != this->agent) return this->state; if (msg.size() < 4) return this->state; if (msg[2] == "C") { auto* const localuser = user->AsLocal(); if (localuser) { ClientProtocol::Message authmsg("AUTHENTICATE"); authmsg.PushParamRef(msg[3]); ClientProtocol::Event authevent(*g_protoev, authmsg); localuser->Send(authevent); } } else if (msg[2] == "D") { this->state = SASL_DONE; this->result = GetSaslResult(msg[3]); } else if (msg[2] == "M") this->user->WriteNumeric(RPL_SASLMECHS, msg[3], "are available SASL mechanisms"); else ServerInstance->Logs.Debug(MODNAME, "Services sent an unknown SASL message \"{}\" \"{}\"", msg[2], msg[3]); break; case SASL_DONE: break; } return this->state; } void Abort() { this->state = SASL_DONE; this->result = SASL_ABORT; } bool SendClientMessage(const std::vector& parameters) { if (this->state != SASL_COMM) return true; SendSASL(this->user, this->agent, 'C', parameters); if (parameters.empty() || parameters[0] == "*") { this->Abort(); return false; } return true; } void AnnounceState() { if (this->state_announced) return; switch (this->result) { case SASL_OK: this->user->WriteNumeric(RPL_SASLSUCCESS, "SASL authentication successful"); break; case SASL_ABORT: this->user->WriteNumeric(ERR_SASLABORTED, "SASL authentication aborted"); break; case SASL_FAIL: this->user->WriteNumeric(ERR_SASLFAIL, "SASL authentication failed"); break; } this->state_announced = true; } }; class CommandAuthenticate final : public SplitCommand { private: // The maximum length of an AUTHENTICATE request. static constexpr size_t MAX_AUTHENTICATE_SIZE = 400; public: Cap::Capability& saslcap; SimpleExtItem& saslext; UserCertificateAPI sslapi; CommandAuthenticate(const WeakModulePtr& mod, SimpleExtItem& ext, Cap::Capability& cap) : SplitCommand(mod, "AUTHENTICATE", 1) , saslcap(cap) , saslext(ext) , sslapi(mod) { works_before_reg = true; } CmdResult HandleLocal(LocalUser* user, const Params& parameters) override { if (!saslcap.IsEnabled(user)) { user->WriteNumeric(ERR_UNKNOWNCOMMAND, this->service_name, "You must request the sasl capability to use this command"); return CmdResult::FAILURE; } if (parameters[0].find(' ') != std::string::npos || parameters[0][0] == ':') { user->WriteNumeric(ERR_SASLABORTED, "SASL authentication aborted"); return CmdResult::FAILURE; } if (parameters[0].length() > MAX_AUTHENTICATE_SIZE) { user->WriteNumeric(ERR_SASLTOOLONG, "SASL message too long"); return CmdResult::FAILURE; } auto* sasl = saslext.Get(user); if (!sasl) saslext.SetFwd(user, user, parameters[0], sslapi); else if (!sasl->SendClientMessage(parameters)) { sasl->AnnounceState(); saslext.Unset(user); } return CmdResult::FAILURE; } }; class CommandSASL final : public Command { private: SimpleExtItem& saslext; public: CommandSASL(const WeakModulePtr& mod, SimpleExtItem& ext) : Command(mod, "SASL", 2) , saslext(ext) { this->access_needed = CmdAccess::SERVER; } CmdResult Handle(User* user, const Params& parameters) override { auto* target = ServerInstance->Users.FindUUID(parameters[1]); if (!target) return CmdResult::FAILURE; // User has gone. auto* sasl = saslext.Get(target); if (!sasl) return CmdResult::FAILURE; // User is not authenticating const auto state = sasl->ProcessInboundMessage(parameters); if (state == SASL_DONE) { sasl->AnnounceState(); saslext.Unset(target); } return CmdResult::SUCCESS; } RouteDescriptor GetRouting(User* user, const Params& parameters) override { return ROUTE_BROADCAST; } }; class ModuleSASL final : public Module { private: Account::ProviderAPI accountproviderapi; SimpleExtItem saslext; SASLCap cap; CommandAuthenticate auth; CommandSASL sasl; ClientProtocol::EventProvider protoev; public: ModuleSASL() : Module(VF_VENDOR, "Provides the IRCv3 sasl client capability.") , accountproviderapi(weak_from_this()) , saslext(weak_from_this(), "sasl-state", ExtensionType::USER) , cap(weak_from_this()) , auth(weak_from_this(), saslext, cap) , sasl(weak_from_this(), saslext) , protoev(weak_from_this(), "AUTHENTICATE") { g_accountproviderapi = &accountproviderapi; g_protoev = &protoev; } void init() override { if (!ServerInstance->Modules.Find("account") || !ServerInstance->Modules.Find("cap")) ServerInstance->Logs.Normal(MODNAME, "WARNING: the cap and services modules are not loaded! The sasl module will NOT function correctly until these two modules are loaded!"); } void OnUserConnect(LocalUser* user) override { // If the client completes connection (with CAP END, NICK, USER and // any other necessary messages) while the SASL authentication is still // in progress, the server SHOULD abort it and send a 906 numeric, then // register the client without authentication. auto* saslauth = saslext.Get(user); if (saslauth && cap.GetProtocol(user) == Cap::CAP_LEGACY) { saslauth->Abort(); saslauth->AnnounceState(); saslext.Unset(user); } } void OnDecodeMetadata(Extensible* target, const std::string& extname, const std::string& extdata) override { if (!target && insp::casemapped_equals(extname, "saslmechlist")) cap.SetMechlist(extdata); } }; MODULE_INIT(ModuleSASL)