1 /* NickServ core functions
2  *
3  * (C) 2003-2020 Anope Team
4  * Contact us at team@anope.org
5  *
6  * Please read COPYING and README for further details.
7  *
8  * Based on the original code of Epona by Lara.
9  * Based on the original code of Services by Andy Church.
10  */
11 
12 #include "module.h"
13 #include "modules/ns_cert.h"
14 
15 static Anope::hash_map<NickCore *> certmap;
16 
17 struct CertServiceImpl : CertService
18 {
CertServiceImplCertServiceImpl19 	CertServiceImpl(Module *o) : CertService(o) { }
20 
FindAccountFromCertCertServiceImpl21 	NickCore* FindAccountFromCert(const Anope::string &cert) anope_override
22 	{
23 		Anope::hash_map<NickCore *>::iterator it = certmap.find(cert);
24 		if (it != certmap.end())
25 			return it->second;
26 		return NULL;
27 	}
28 };
29 
30 struct NSCertListImpl : NSCertList
31 {
32 	Serialize::Reference<NickCore> nc;
33 	std::vector<Anope::string> certs;
34 
35  public:
NSCertListImplNSCertListImpl36 	NSCertListImpl(Extensible *obj) : nc(anope_dynamic_static_cast<NickCore *>(obj)) { }
37 
~NSCertListImplNSCertListImpl38 	~NSCertListImpl()
39 	{
40 		ClearCert();
41 	}
42 
43 	/** Add an entry to the nick's certificate list
44 	 *
45 	 * @param entry The fingerprint to add to the cert list
46 	 *
47 	 * Adds a new entry into the cert list.
48 	 */
AddCertNSCertListImpl49 	void AddCert(const Anope::string &entry) anope_override
50 	{
51 		this->certs.push_back(entry);
52 		certmap[entry] = nc;
53 		FOREACH_MOD(OnNickAddCert, (this->nc, entry));
54 	}
55 
56 	/** Get an entry from the nick's cert list by index
57 	 *
58 	 * @param entry Index in the certificaate list vector to retrieve
59 	 * @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
60 	 *
61 	 * Retrieves an entry from the certificate list corresponding to the given index.
62 	 */
GetCertNSCertListImpl63 	Anope::string GetCert(unsigned entry) const anope_override
64 	{
65 		if (entry >= this->certs.size())
66 			return "";
67 		return this->certs[entry];
68 	}
69 
GetCertCountNSCertListImpl70 	unsigned GetCertCount() const anope_override
71 	{
72 		return this->certs.size();
73 	}
74 
75 	/** Find an entry in the nick's cert list
76 	 *
77 	 * @param entry The fingerprint to search for
78 	 * @return True if the fingerprint is found in the cert list, false otherwise
79 	 *
80 	 * Search for an fingerprint within the cert list.
81 	 */
FindCertNSCertListImpl82 	bool FindCert(const Anope::string &entry) const anope_override
83 	{
84 		return std::find(this->certs.begin(), this->certs.end(), entry) != this->certs.end();
85 	}
86 
87 	/** Erase a fingerprint from the nick's certificate list
88 	 *
89 	 * @param entry The fingerprint to remove
90 	 *
91 	 * Removes the specified fingerprint from the cert list.
92 	 */
EraseCertNSCertListImpl93 	void EraseCert(const Anope::string &entry) anope_override
94 	{
95 		std::vector<Anope::string>::iterator it = std::find(this->certs.begin(), this->certs.end(), entry);
96 		if (it != this->certs.end())
97 		{
98 			FOREACH_MOD(OnNickEraseCert, (this->nc, entry));
99 			certmap.erase(entry);
100 			this->certs.erase(it);
101 		}
102 	}
103 
104 	/** Clears the entire nick's cert list
105 	 *
106 	 * Deletes all the memory allocated in the certificate list vector and then clears the vector.
107 	 */
ClearCertNSCertListImpl108 	void ClearCert() anope_override
109 	{
110 		FOREACH_MOD(OnNickClearCert, (this->nc));
111 		for (unsigned i = 0; i < certs.size(); ++i)
112 			certmap.erase(certs[i]);
113 		this->certs.clear();
114 	}
115 
CheckNSCertListImpl116 	void Check() anope_override
117 	{
118 		if (this->certs.empty())
119 			nc->Shrink<NSCertList>("certificates");
120 	}
121 
122 	struct ExtensibleItem : ::ExtensibleItem<NSCertListImpl>
123 	{
ExtensibleItemNSCertListImpl::ExtensibleItem124 		ExtensibleItem(Module *m, const Anope::string &ename) : ::ExtensibleItem<NSCertListImpl>(m, ename) { }
125 
ExtensibleSerializeNSCertListImpl::ExtensibleItem126 		void ExtensibleSerialize(const Extensible *e, const Serializable *s, Serialize::Data &data) const anope_override
127 		{
128 			if (s->GetSerializableType()->GetName() != "NickCore")
129 				return;
130 
131 			const NickCore *n = anope_dynamic_static_cast<const NickCore *>(e);
132 			NSCertList *c = this->Get(n);
133 			if (c == NULL || !c->GetCertCount())
134 				return;
135 
136 			for (unsigned i = 0; i < c->GetCertCount(); ++i)
137 				data["cert"] << c->GetCert(i) << " ";
138 		}
139 
ExtensibleUnserializeNSCertListImpl::ExtensibleItem140 		void ExtensibleUnserialize(Extensible *e, Serializable *s, Serialize::Data &data) anope_override
141 		{
142 			if (s->GetSerializableType()->GetName() != "NickCore")
143 				return;
144 
145 			NickCore *n = anope_dynamic_static_cast<NickCore *>(e);
146 			NSCertListImpl *c = this->Require(n);
147 
148 			Anope::string buf;
149 			data["cert"] >> buf;
150 			spacesepstream sep(buf);
151 			for (unsigned i = 0; i < c->certs.size(); ++i)
152 				certmap.erase(c->certs[i]);
153 			c->certs.clear();
154 			while (sep.GetToken(buf))
155 			{
156 				c->certs.push_back(buf);
157 				certmap[buf] = n;
158 			}
159 		}
160 	};
161 };
162 
163 class CommandNSCert : public Command
164 {
165  private:
DoAdd(CommandSource & source,NickCore * nc,Anope::string certfp)166 	void DoAdd(CommandSource &source, NickCore *nc, Anope::string certfp)
167 	{
168 		NSCertList *cl = nc->Require<NSCertList>("certificates");
169 		unsigned max = Config->GetModule(this->owner)->Get<unsigned>("max", "5");
170 
171 		if (cl->GetCertCount() >= max)
172 		{
173 			source.Reply(_("Sorry, the maximum of %d certificate entries has been reached."), max);
174 			return;
175 		}
176 
177 		if (source.GetAccount() == nc)
178 		{
179 			User *u = source.GetUser();
180 
181 			if (!u || u->fingerprint.empty())
182 			{
183 				source.Reply(_("You are not using a client certificate."));
184 				return;
185 			}
186 
187 			certfp = u->fingerprint;
188 		}
189 
190 		if (cl->FindCert(certfp))
191 		{
192 			source.Reply(_("Fingerprint \002%s\002 already present on %s's certificate list."), certfp.c_str(), nc->display.c_str());
193 			return;
194 		}
195 
196 		if (certmap.find(certfp) != certmap.end())
197 		{
198 			source.Reply(_("Fingerprint \002%s\002 is already in use."), certfp.c_str());
199 			return;
200 		}
201 
202 		cl->AddCert(certfp);
203 		Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to ADD certificate fingerprint " << certfp << " to " << nc->display;
204 		source.Reply(_("\002%s\002 added to %s's certificate list."), certfp.c_str(), nc->display.c_str());
205 	}
206 
DoDel(CommandSource & source,NickCore * nc,Anope::string certfp)207 	void DoDel(CommandSource &source, NickCore *nc, Anope::string certfp)
208 	{
209 		NSCertList *cl = nc->Require<NSCertList>("certificates");
210 
211 		if (certfp.empty())
212 		{
213 			User *u = source.GetUser();
214 			if (u)
215 				certfp = u->fingerprint;
216 		}
217 
218 		if (certfp.empty())
219 		{
220 			this->OnSyntaxError(source, "DEL");
221 			return;
222 		}
223 
224 		if (!cl->FindCert(certfp))
225 		{
226 			source.Reply(_("\002%s\002 not found on %s's certificate list."), certfp.c_str(), nc->display.c_str());
227 			return;
228 		}
229 
230 		cl->EraseCert(certfp);
231 		cl->Check();
232 		Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to DELETE certificate fingerprint " << certfp << " from " << nc->display;
233 		source.Reply(_("\002%s\002 deleted from %s's certificate list."), certfp.c_str(), nc->display.c_str());
234 	}
235 
DoList(CommandSource & source,const NickCore * nc)236 	void DoList(CommandSource &source, const NickCore *nc)
237 	{
238 		NSCertList *cl = nc->GetExt<NSCertList>("certificates");
239 
240 		if (!cl || !cl->GetCertCount())
241 		{
242 			source.Reply(_("%s's certificate list is empty."), nc->display.c_str());
243 			return;
244 		}
245 
246 		source.Reply(_("Certificate list for %s:"), nc->display.c_str());
247 		for (unsigned i = 0; i < cl->GetCertCount(); ++i)
248 		{
249 			Anope::string fingerprint = cl->GetCert(i);
250 			source.Reply("    %s", fingerprint.c_str());
251 		}
252 	}
253 
254  public:
CommandNSCert(Module * creator)255 	CommandNSCert(Module *creator) : Command(creator, "nickserv/cert", 1, 3)
256 	{
257 		this->SetDesc(_("Modify the nickname client certificate list"));
258 		this->SetSyntax(_("ADD [\037nickname\037] [\037fingerprint\037]"));
259 		this->SetSyntax(_("DEL [\037nickname\037] \037fingerprint\037"));
260 		this->SetSyntax(_("LIST [\037nickname\037]"));
261 	}
262 
Execute(CommandSource & source,const std::vector<Anope::string> & params)263 	void Execute(CommandSource &source, const std::vector<Anope::string> &params) anope_override
264 	{
265 		const Anope::string &cmd = params[0];
266 		Anope::string nick, certfp;
267 
268 		if (cmd.equals_ci("LIST"))
269 			nick = params.size() > 1 ? params[1] : "";
270 		else
271 		{
272 			nick = params.size() == 3 ? params[1] : "";
273 			certfp = params.size() > 1 ? params[params.size() - 1] : "";
274 		}
275 
276 		NickCore *nc;
277 		if (!nick.empty())
278 		{
279 			const NickAlias *na = NickAlias::Find(nick);
280 			if (na == NULL)
281 			{
282 				source.Reply(NICK_X_NOT_REGISTERED, nick.c_str());
283 				return;
284 			}
285 			else if (na->nc != source.GetAccount() && !source.HasPriv("nickserv/access"))
286 			{
287 				source.Reply(ACCESS_DENIED);
288 				return;
289 			}
290 			else if (Config->GetModule("nickserv")->Get<bool>("secureadmins", "yes") && source.GetAccount() != na->nc && na->nc->IsServicesOper() && !cmd.equals_ci("LIST"))
291 			{
292 				source.Reply(_("You may view but not modify the certificate list of other Services Operators."));
293 				return;
294 			}
295 
296 			nc = na->nc;
297 		}
298 		else
299 			nc = source.nc;
300 
301 		if (cmd.equals_ci("LIST"))
302 			return this->DoList(source, nc);
303 		else if (nc->HasExt("NS_SUSPENDED"))
304 			source.Reply(NICK_X_SUSPENDED, nc->display.c_str());
305 		else if (Anope::ReadOnly)
306 			source.Reply(READ_ONLY_MODE);
307 		else if (cmd.equals_ci("ADD"))
308 			return this->DoAdd(source, nc, certfp);
309 		else if (cmd.equals_ci("DEL"))
310 			return this->DoDel(source, nc, certfp);
311 		else
312 			this->OnSyntaxError(source, "");
313 	}
314 
OnHelp(CommandSource & source,const Anope::string & subcommand)315 	bool OnHelp(CommandSource &source, const Anope::string &subcommand) anope_override
316 	{
317 		this->SendSyntax(source);
318 		source.Reply(" ");
319 		source.Reply(_("Modifies or displays the certificate list for your nick.\n"
320 				"If you connect to IRC and provide a client certificate with a\n"
321 				"matching fingerprint in the cert list, you will be\n"
322 				"automatically identified to services. Services Operators\n"
323 				"may provide a nick to modify other users' certificate lists.\n"
324 				" \n"));
325 		source.Reply(_("Examples:\n"
326 				" \n"
327 				"    \002CERT ADD\002\n"
328 				"        Adds your current fingerprint to the certificate list and\n"
329 				"        automatically identifies you when you connect to IRC\n"
330 				"        using this fingerprint.\n"
331 				" \n"
332 				"    \002CERT DEL <fingerprint>\002\n"
333 				"        Removes the fingerprint <fingerprint> from your certificate list.\n"
334 				" \n"
335 				"    \002CERT LIST\002\n"
336 				"        Displays the current certificate list."));
337 		return true;
338 	}
339 };
340 
341 class NSCert : public Module
342 {
343 	CommandNSCert commandnscert;
344 	NSCertListImpl::ExtensibleItem certs;
345 	CertServiceImpl cs;
346 
347  public:
NSCert(const Anope::string & modname,const Anope::string & creator)348 	NSCert(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
349 		commandnscert(this), certs(this, "certificates"), cs(this)
350 	{
351 		if (!IRCD || !IRCD->CanCertFP)
352 			throw ModuleException("Your IRCd does not support ssl client certificates");
353 	}
354 
OnFingerprint(User * u)355 	void OnFingerprint(User *u) anope_override
356 	{
357 		BotInfo *NickServ = Config->GetClient("NickServ");
358 		if (!NickServ || u->IsIdentified())
359 			return;
360 
361 		NickCore *nc = cs.FindAccountFromCert(u->fingerprint);
362 		if (!nc || nc->HasExt("NS_SUSPENDED"))
363 			return;
364 
365 		unsigned int maxlogins = Config->GetModule("ns_identify")->Get<unsigned int>("maxlogins");
366 		if (maxlogins && nc->users.size() >= maxlogins)
367 		{
368 			u->SendMessage(NickServ, _("Account \002%s\002 has already reached the maximum number of simultaneous logins (%u)."), nc->display.c_str(), maxlogins);
369 			return;
370 		}
371 
372 		NickAlias *na = NickAlias::Find(u->nick);
373 		if (na && na->nc == nc)
374 			u->Identify(na);
375 		else
376 			u->Login(nc);
377 
378 		u->SendMessage(NickServ, _("SSL certificate fingerprint accepted, you are now identified to \002%s\002."), nc->display.c_str());
379 		Log(NickServ) << u->GetMask() << " automatically identified for account " << nc->display << " via SSL certificate fingerprint";
380 	}
381 
OnNickValidate(User * u,NickAlias * na)382 	EventReturn OnNickValidate(User *u, NickAlias *na) anope_override
383 	{
384 		NSCertList *cl = certs.Get(na->nc);
385 		if (!u->fingerprint.empty() && cl && cl->FindCert(u->fingerprint))
386 		{
387 			BotInfo *NickServ = Config->GetClient("NickServ");
388 
389 			unsigned int maxlogins = Config->GetModule("ns_identify")->Get<unsigned int>("maxlogins");
390 			if (maxlogins && na->nc->users.size() >= maxlogins)
391 			{
392 				u->SendMessage(NickServ, _("Account \002%s\002 has already reached the maximum number of simultaneous logins (%u)."), na->nc->display.c_str(), maxlogins);
393 				return EVENT_CONTINUE;
394 			}
395 
396 			u->Identify(na);
397 
398 			u->SendMessage(NickServ, _("SSL certificate fingerprint accepted, you are now identified."));
399 			Log(NickServ) << u->GetMask() << " automatically identified for account " << na->nc->display << " via SSL certificate fingerprint";
400 			return EVENT_ALLOW;
401 		}
402 
403 		return EVENT_CONTINUE;
404 	}
405 };
406 
407 MODULE_INIT(NSCert)
408