From 37457105f33385133425cce03585ef993490d948 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Mon, 24 Feb 2025 20:01:34 +0000 Subject: [PATCH] Support SASL EXTERNAL in certauth --- modules/certauth.cpp | 87 +++++++++++++++++++++++++++--- modules/saslplainauth.cpp | 4 +- test/integration/tests/modules.cpp | 80 +++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 10 deletions(-) diff --git a/modules/certauth.cpp b/modules/certauth.cpp index 1875ad4b..2a6dbecc 100644 --- a/modules/certauth.cpp +++ b/modules/certauth.cpp @@ -53,15 +53,18 @@ class CSSLClientCertMod : public CModule { for (MCString::const_iterator it = BeginNV(); it != EndNV(); ++it) { VCString vsKeys; + const CString& sUser = it->first; - if (CZNC::Get().FindUser(it->first) == nullptr) { - DEBUG("Unknown user in saved data [" + it->first + "]"); + if (CZNC::Get().FindUser(sUser) == nullptr) { + DEBUG("Unknown user in saved data [" + sUser + "]"); continue; } it->second.Split(" ", vsKeys, false); - for (const CString& sKey : vsKeys) { - m_PubKeys[it->first].insert(sKey.AsLower()); + for (CString& sKey : vsKeys) { + sKey.MakeLower(); + m_PubKeys[sUser].insert(sKey); + m_KeyToUser[sKey].insert(sUser); } } @@ -90,12 +93,14 @@ class CSSLClientCertMod : public CModule { return SaveRegistry(); } - bool AddKey(CUser* pUser, const CString& sKey) { + bool AddKey(CUser* pUser, CString sKey) { + sKey.MakeLower(); const pair pair = - m_PubKeys[pUser->GetUsername()].insert(sKey.AsLower()); + m_PubKeys[pUser->GetUsername()].insert(sKey); if (pair.second) { Save(); + m_KeyToUser[sKey].insert(pUser->GetUsername()); } return pair.second; @@ -118,7 +123,7 @@ class CSSLClientCertMod : public CModule { MSCString::const_iterator it = m_PubKeys.find(sUser); if (it == m_PubKeys.end()) { - DEBUG("No saved pubkeys for this client"); + DEBUG("No saved pubkeys for this user"); return CONTINUE; } @@ -135,6 +140,56 @@ class CSSLClientCertMod : public CModule { return HALT; } + void OnClientGetSASLMechanisms(SCString& ssMechanisms) override { + ssMechanisms.insert("EXTERNAL"); + } + + EModRet OnClientSASLAuthenticate(const CString& sMechanism, + const CString& sMessage) override { + if (sMechanism != "EXTERNAL") { + return CONTINUE; + } + CString sUser = GetClient()->ParseUser(sMessage); + const CString sKey = GetKey(GetClient()); + DEBUG("Key: " << sKey); + + if (sKey.empty()) { + GetClient()->RefuseSASLLogin("No client cert presented"); + return HALT; + } + + auto it = m_KeyToUser.find(sKey); + if (it == m_KeyToUser.end()) { + GetClient()->RefuseSASLLogin("Client cert not recognized"); + return HALT; + } + + const SCString& ssUsers = it->second; + + if (ssUsers.empty()) { + GetClient()->RefuseSASLLogin("Key found, but list of users is empty, please report bug"); + return HALT; + } + + if (sUser.empty()) { + sUser = *ssUsers.begin(); + } else if (ssUsers.count(sUser) == 0) { + GetClient()->RefuseSASLLogin( + "The specified user doesn't have this key"); + return HALT; + } + + CUser* pUser = CZNC::Get().FindUser(sUser); + if (!pUser) { + GetClient()->RefuseSASLLogin("User not found"); + return HALT; + } + + DEBUG("Accepted cert auth for " << sUser); + GetClient()->AcceptSASLLogin(*pUser); + return HALT; + } + void HandleShowCommand(const CString& sLine) { const CString sPubKey = GetKey(GetClient()); @@ -211,8 +266,16 @@ class CSSLClientCertMod : public CModule { id--; } + CString sKey = *it2; it->second.erase(it2); if (it->second.size() == 0) m_PubKeys.erase(it); + + it = m_KeyToUser.find(sKey); + if (it != m_KeyToUser.end()) { + it->second.erase(GetUser()->GetUsername()); + if (it->second.empty()) m_KeyToUser.erase(it); + } + PutModule(t_s("Removed")); Save(); @@ -259,11 +322,18 @@ class CSSLClientCertMod : public CModule { } else if (sPageName == "delete") { MSCString::iterator it = m_PubKeys.find(pUser->GetUsername()); if (it != m_PubKeys.end()) { - if (it->second.erase(WebSock.GetParam("key", false))) { + CString sKey = WebSock.GetParam("key", false); + if (it->second.erase(sKey)) { if (it->second.size() == 0) { m_PubKeys.erase(it); } + it = m_KeyToUser.find(sKey); + if (it != m_KeyToUser.end()) { + it->second.erase(pUser->GetUsername()); + if (it->second.empty()) m_KeyToUser.erase(it); + } + Save(); } } @@ -279,6 +349,7 @@ class CSSLClientCertMod : public CModule { // Maps user names to a list of allowed pubkeys typedef map> MSCString; MSCString m_PubKeys; + MSCString m_KeyToUser; }; template <> diff --git a/modules/saslplainauth.cpp b/modules/saslplainauth.cpp index 4cf7a7e4..bc23cd4d 100644 --- a/modules/saslplainauth.cpp +++ b/modules/saslplainauth.cpp @@ -19,7 +19,7 @@ class CSASLMechanismPlain : public CModule { public: - MODCONSTRUCTOR(CSASLMechanismPlain) { AddHelpCommand(); } + MODCONSTRUCTOR(CSASLMechanismPlain) {} void OnClientGetSASLMechanisms(SCString& ssMechanisms) override { ssMechanisms.insert("PLAIN"); @@ -27,7 +27,7 @@ class CSASLMechanismPlain : public CModule { EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sMessage) override { - if (!sMechanism.Equals("PLAIN")) { + if (sMechanism != "PLAIN") { return CONTINUE; } diff --git a/test/integration/tests/modules.cpp b/test/integration/tests/modules.cpp index 2c7efd1b..6863e6c5 100644 --- a/test/integration/tests/modules.cpp +++ b/test/integration/tests/modules.cpp @@ -19,6 +19,8 @@ #include "znctest.h" +#include + using testing::HasSubstr; using testing::Not; @@ -362,5 +364,83 @@ TEST_F(ZNCTest, SaslAuthPlain) { client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); } +TEST_F(ZNCTest, SaslAuthExternal) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + ircd.Write(":server 001 nick :Hello"); + auto client = LoginClient(); + client.Write("znc addport +12346 all all"); + client.ReadUntil(":Port added"); + client.Write("znc loadmod certauth"); + client.ReadUntil("Loaded"); + client.Close(); + + QSslSocket sock; + // Could generate a new one for the test, but this one is good enough + sock.setLocalCertificate(m_dir.path() + "/znc.pem"); + sock.setPrivateKey(m_dir.path() + "/znc.pem"); + sock.setPeerVerifyMode(QSslSocket::VerifyNone); + sock.connectToHostEncrypted("127.0.0.1", 12346); + ASSERT_TRUE(sock.waitForConnected()) << sock.errorString().toStdString(); + ASSERT_TRUE(sock.waitForEncrypted()) << sock.errorString().toStdString(); + auto client2 = WrapIO(&sock); + client2.Write("PASS :hunter2"); + client2.Write("NICK nick"); + client2.Write("USER user/test x x :x"); + client2.Write("privmsg *certauth add"); + client2.ReadUntil("added"); + + auto Reconnect = [&] { + client2.Close(); + ASSERT_TRUE(sock.state() == QAbstractSocket::UnconnectedState || sock.waitForDisconnected()) + << sock.errorString().toStdString(); + sock.connectToHostEncrypted("127.0.0.1", 12346); + ASSERT_TRUE(sock.waitForConnected()) + << sock.errorString().toStdString(); + ASSERT_TRUE(sock.waitForEncrypted()) + << sock.errorString().toStdString(); + client2.Write("CAP REQ sasl"); + client2.Write("NICK nick"); + client2.Write("USER u x x :x"); + client2.ReadUntil("ACK :sasl"); + client2.Write("AUTHENTICATE EXTERNAL"); + client2.ReadUntil("AUTHENTICATE +"); + }; + + Reconnect(); + ircd.Write(":friend PRIVMSG nick :hello"); + client2.Write("AUTHENTICATE +"); + client2.ReadUntil( + ":irc.znc.in 900 nick nick!user@127.0.0.1 user :You are now logged in " + "as user"); + client2.ReadUntil(":irc.znc.in 903 nick :SASL authentication successful"); + client2.Write("CAP END"); + // '[' comes from lack of server-time + client2.ReadUntil(":friend PRIVMSG nick :["); + + Reconnect(); + client2.Write("AUTHENTICATE " + QString("user/te").toUtf8().toBase64()); + client2.ReadUntil( + ":irc.znc.in 900 nick nick!user@127.0.0.1 user :You are now logged in " + "as user"); + client2.ReadUntil(":irc.znc.in 903 nick :SASL authentication successful"); + client2.Write("CAP END"); + client2.ReadUntil( + ":*status!status@znc.in PRIVMSG nick :Network te doesn't exist."); + + Reconnect(); + client2.Write("AUTHENTICATE " + QString("moo").toUtf8().toBase64()); + client2.ReadUntil( + ":irc.znc.in 904 nick :The specified user doesn't have this key"); + + client = LoginClient(); + client.Write("privmsg *certauth :del 1"); + client.ReadUntil("Removed"); + Reconnect(); + client2.Write("AUTHENTICATE +"); + client2.ReadUntil( + ":irc.znc.in 904 nick :Client cert not recognized"); +} + } // namespace } // namespace znc_inttest