From ebd03383f856bfaa72b1700a96561396e6f8f6cc Mon Sep 17 00:00:00 2001 From: Sadie Powell Date: Mon, 19 Feb 2024 18:46:05 +0000 Subject: Allow using multiple SSL fingerprint algorithms. Closes #1804. --- docs/conf/modules.conf.example | 9 ++--- include/hashcomp.h | 6 --- include/modules/ssl.h | 37 +++++++++++++---- src/hashcomp.cpp | 10 ----- src/modules/extra/m_ssl_gnutls.cpp | 76 ++++++++++++++++++++--------------- src/modules/extra/m_ssl_mbedtls.cpp | 35 +++++++++++----- src/modules/extra/m_ssl_openssl.cpp | 32 +++++++++------ src/modules/m_gateway.cpp | 21 ++++++++-- src/modules/m_sasl.cpp | 8 ++-- src/modules/m_spanningtree/compat.cpp | 29 +++++++++++++ src/modules/m_sslinfo.cpp | 69 +++++++++++++++++++++---------- src/modules/m_sslmodes.cpp | 11 ++++- 12 files changed, 228 insertions(+), 115 deletions(-) diff --git a/docs/conf/modules.conf.example b/docs/conf/modules.conf.example index 6e7dafd76..7c19f620d 100644 --- a/docs/conf/modules.conf.example +++ b/docs/conf/modules.conf.example @@ -2464,7 +2464,9 @@ # gateway users (requires the gateway module). This # # should be the same algorithm you specified in the # # field of the TLS profile used for # -# user connections. # +# user connections. You can prefix the algorithm name # +# with spki- to use a Subject Public Key Info (SPKI) # +# fingerprint instead of a certificate fingerprint. # # # # localsecure - Whether to treat locally-connected plaintext users # # as if they are connected with TLS. Defaults to yes. # @@ -2472,10 +2474,6 @@ # operonly - Whether TLS client certificate info is only visible # # by server operators. Defaults to no. # # # -# spkifp - Whether to use a Subject Public Key Info (SPKI) # -# fingerprint instead of a certificate fingerprint # -# for user TLS client fingerprints. Defaults to no. # -# # # warnexpiring - If specified then the maximum period of validity # # that can be left on a user's TLS client certificate # # before users are warned about the imminent expiry. # @@ -2489,7 +2487,6 @@ # diff --git a/include/hashcomp.h b/include/hashcomp.h index ec6cf34f8..f7509a444 100644 --- a/include/hashcomp.h +++ b/include/hashcomp.h @@ -156,12 +156,6 @@ namespace irc * @return True if the end of the stream has been reached, otherwise false */ bool StreamEnd(); - - /** Returns true if the specified value exists in the stream - * @param value The value to search for - * @return True if the value was found, False otherwise - */ - bool Contains(const std::string& value); }; /** A derived form of sepstream, which separates on commas diff --git a/include/modules/ssl.h b/include/modules/ssl.h index 1540f6385..bde05a5b8 100644 --- a/include/modules/ssl.h +++ b/include/modules/ssl.h @@ -45,7 +45,7 @@ public: std::string dn; std::string issuer; std::string error; - std::string fingerprint; + std::vector fingerprints; bool trusted = false; bool invalid = true; bool unknownsigner = true; @@ -79,12 +79,20 @@ public: return error; } - /** Get key fingerprint. - * @return The key fingerprint as a hex string. + /** Get primary fingerprint. + * @return The primary fingerprint as a hex string. */ - const std::string& GetFingerprint() const + std::string GetFingerprint() const { - return fingerprint; + return fingerprints.empty() ? "" : fingerprints.front(); + } + + /** Get all fingerprints. + * @return All fingerprints as a hex string. + */ + const std::vector& GetFingerprints() const + { + return fingerprints; } /** Get trust status @@ -333,9 +341,9 @@ public: */ virtual void SetCertificate(User* user, ssl_cert* cert) = 0; - /** Get the key fingerprint from a user's certificate - * @param user The user whose key fingerprint to get, user may be remote - * @return The key fingerprint from the user's TLS certificate or an empty string + /** Get the primary fingerprint from a user's certificate + * @param user The user whose primary fingerprint to get. + * @return The primary fingerprint from the user's TLS certificate or an empty string * if the user is not using TLS or did not provide a client certificate */ std::string GetFingerprint(User* user) @@ -345,6 +353,19 @@ public: return cert->GetFingerprint(); return ""; } + + /** Get all fingerprint from a user's certificate + * @param user The user whose fingerprint to get. + * @return The fingerprint from the user's TLS certificate or an empty string + * if the user is not using TLS or did not provide a client certificate + */ + std::vector GetFingerprints(User* user) + { + ssl_cert* cert = GetCertificate(user); + if (cert) + return cert->GetFingerprints(); + return {}; + } }; /** API implemented by m_sslinfo that allows modules to retrieve the TLS certificate diff --git a/src/hashcomp.cpp b/src/hashcomp.cpp index 592485124..103098c62 100644 --- a/src/hashcomp.cpp +++ b/src/hashcomp.cpp @@ -262,16 +262,6 @@ bool irc::sepstream::StreamEnd() return this->pos > this->tokens.length(); } -bool irc::sepstream::Contains(const std::string& value) -{ - std::string token; - while (GetToken(token)) - if (value == token) - return true; - - return false; -} - irc::portparser::portparser(const std::string& source, bool allow_overlapped) : sep(source) , overlapped(allow_overlapped) diff --git a/src/modules/extra/m_ssl_gnutls.cpp b/src/modules/extra/m_ssl_gnutls.cpp index 2cd3badaf..3078ae824 100644 --- a/src/modules/extra/m_ssl_gnutls.cpp +++ b/src/modules/extra/m_ssl_gnutls.cpp @@ -117,24 +117,37 @@ namespace GnuTLS class Hash final { - gnutls_digest_algorithm_t hash; + std::vector> hashes; public: // Nothing to deallocate, constructor may throw freely - Hash(const std::string& hashname) + Hash(const std::string& hashlist) { - hash = gnutls_digest_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, nullptr); + irc::spacesepstream hashstream(hashlist); + for (std::string hashname; hashstream.GetToken(hashname); ) + { + bool spki = false; + if (!hashname.compare(0, 5, "spki-", 5) && hashname.length() > 5) + { + spki = true; + hashname.erase(0, 5); + } + + auto hash = gnutls_digest_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, nullptr); + + hashes.emplace_back(hash, spki); + } } - gnutls_digest_algorithm_t get() const { return hash; } + std::vector> get() const { return hashes; } }; #ifndef GNUTLS_AUTO_DH @@ -482,9 +495,6 @@ namespace GnuTLS */ const bool requestclientcert; - /** Whether the fingerprint is a SPKI fingerprint or not. */ - bool spkifp; - static std::string ReadFile(const std::string& filename) { auto file = ServerInstance->Config->ReadFile(filename, ServerInstance->Time()); @@ -538,7 +548,6 @@ namespace GnuTLS unsigned int outrecsize; bool requestclientcert; - bool spkifp; Config(const std::string& profilename, const std::shared_ptr& tag) : name(profilename) @@ -551,7 +560,6 @@ namespace GnuTLS , mindh(tag->getNum("mindhbits", 1024)) , hashstr(tag->getString("hash", "sha256", 1)) , requestclientcert(tag->getBool("requestclientcert", true)) - , spkifp(tag->getBool("spkifp")) { // Load trusted CA and revocation list, if set std::string filename = tag->getString("cafile"); @@ -576,7 +584,6 @@ namespace GnuTLS , priority(config.priostr) , outrecsize(config.outrecsize) , requestclientcert(config.requestclientcert) - , spkifp(config.spkifp) { #ifndef GNUTLS_AUTO_DH x509cred.SetDH(config.dh); @@ -598,9 +605,8 @@ namespace GnuTLS const std::string& GetName() const { return name; } X509Credentials& GetX509Credentials() { return x509cred; } - gnutls_digest_algorithm_t GetHash() const { return hash.get(); } + std::vector> GetHash() const { return hash.get(); } unsigned int GetOutgoingRecordSize() const { return outrecsize; } - bool UseSPKI() const { return spkifp; } }; } @@ -669,12 +675,12 @@ private: } } - int GetCertFP(gnutls_x509_crt_t& cert, char* buffer, size_t& buffer_size) + int GetCertFP(gnutls_x509_crt_t& cert, gnutls_digest_algorithm_t algo, char* buffer, size_t& buffer_size) { - return gnutls_x509_crt_get_fingerprint(cert, GetProfile().GetHash(), buffer, &buffer_size); + return gnutls_x509_crt_get_fingerprint(cert, algo, buffer, &buffer_size); } - int GetSPKIFP(gnutls_x509_crt_t& cert, char* buffer, size_t& buffer_size) + int GetSPKIFP(gnutls_x509_crt_t& cert, gnutls_digest_algorithm_t algo, char* buffer, size_t& buffer_size) { int ret; gnutls_pubkey_t pubkey; @@ -698,10 +704,10 @@ private: return ret; gnutls_pubkey_deinit(pubkey); - if (gnutls_hash_fast(GetProfile().GetHash(), &derkey[0], derkey_size, buffer) != GNUTLS_E_SUCCESS) + if (gnutls_hash_fast(algo, &derkey[0], derkey_size, buffer) != GNUTLS_E_SUCCESS) return ret; - buffer_size = gnutls_hash_get_len(GetProfile().GetHash()); + buffer_size = gnutls_hash_get_len(algo); return GNUTLS_E_SUCCESS; } @@ -762,15 +768,19 @@ private: if (gnutls_x509_crt_get_issuer_dn(cert, buffer, &buffer_size) == 0) ProcessDNString(buffer, buffer_size, certinfo->issuer); - buffer_size = sizeof(buffer); - ret = GetProfile().UseSPKI() ? GetSPKIFP(cert, buffer, buffer_size) : GetCertFP(cert, buffer, buffer_size); - if (ret != GNUTLS_E_SUCCESS) - { - certinfo->error = gnutls_strerror(ret); - } - else + + for (const auto& [hash, spki] : GetProfile().GetHash()) { - certinfo->fingerprint = Hex::Encode(buffer, buffer_size); + buffer_size = sizeof(buffer); + ret = spki ? GetSPKIFP(cert, hash, buffer, buffer_size) : GetCertFP(cert, hash, buffer, buffer_size); + if (ret != GNUTLS_E_SUCCESS) + { + certinfo->error = gnutls_strerror(ret); + } + else + { + certinfo->fingerprints.push_back(Hex::Encode(buffer, buffer_size)); + } } certinfo->activation = gnutls_x509_crt_get_activation_time(cert); diff --git a/src/modules/extra/m_ssl_mbedtls.cpp b/src/modules/extra/m_ssl_mbedtls.cpp index 9e72cb49c..5890db067 100644 --- a/src/modules/extra/m_ssl_mbedtls.cpp +++ b/src/modules/extra/m_ssl_mbedtls.cpp @@ -368,27 +368,40 @@ namespace mbedTLS class Hash final { - const mbedtls_md_info_t* md; + private: + std::vector> mds; /** Buffer where cert hashes are written temporarily */ mutable std::vector buf; public: - Hash(std::string hashstr) + Hash(const std::string& hashstr) { - std::transform(hashstr.begin(), hashstr.end(), hashstr.begin(), ::toupper); - md = mbedtls_md_info_from_string(hashstr.c_str()); - if (!md) - throw Exception("Unknown hash: " + hashstr); + irc::spacesepstream hashstream(hashstr); + unsigned char mdsize = 0; + for (std::string hash; hashstream.GetToken(hash); ) + { + std::transform(hash.begin(), hash.end(), hash.begin(), ::toupper); + const auto* md = mbedtls_md_info_from_string(hash.c_str()); + if (!md) + throw Exception("Unknown hash: " + hash); + + mds.push_back(std::make_pair(md, mbedtls_md_get_size(md))); + mdsize = std::max(mdsize, mds.back().second); + } - buf.resize(mbedtls_md_get_size(md)); + buf.resize(mdsize); + buf.shrink_to_fit(); } - std::string hash(const unsigned char* input, size_t length) const + void hash(const unsigned char* input, size_t length, std::vector& fingerprints) const { - mbedtls_md(md, input, length, &buf.front()); - return Hex::Encode(&buf.front(), buf.size()); + for (const auto& [md, size] : mds) + { + mbedtls_md(md, input, length, &buf.front()); + fingerprints.push_back(Hex::Encode(&buf.front(), size)); + } } }; @@ -627,7 +640,7 @@ private: } // If there is a certificate we can always generate a fingerprint - certificate->fingerprint = GetProfile().GetHash().hash(cert->raw.p, cert->raw.len); + GetProfile().GetHash().hash(cert->raw.p, cert->raw.len, certificate->fingerprints); // At this point mbedTLS verified the cert already, we just need to check the results const uint32_t flags = mbedtls_ssl_get_verify_result(&sess); diff --git a/src/modules/extra/m_ssl_openssl.cpp b/src/modules/extra/m_ssl_openssl.cpp index 9f7240583..857755c0f 100644 --- a/src/modules/extra/m_ssl_openssl.cpp +++ b/src/modules/extra/m_ssl_openssl.cpp @@ -303,7 +303,7 @@ namespace OpenSSL /** Digest to use when generating fingerprints */ - const EVP_MD* digest; + std::vector digests; /** Last error, set by error_callback() */ @@ -382,10 +382,15 @@ namespace OpenSSL throw Exception("Couldn't set DH parameters"); #endif - const std::string hash = tag->getString("hash", "sha256", 1); - digest = EVP_get_digestbyname(hash.c_str()); - if (!digest) - throw Exception("Unknown hash type " + hash); + irc::spacesepstream hashstream(tag->getString("hash", "sha256", 1)); + for (std::string hash; hashstream.GetToken(hash); ) + { + const auto* digest = EVP_get_digestbyname(hash.c_str()); + if (!digest) + throw Exception("Unknown hash type " + hash); + + digests.push_back(digest); + } const std::string ciphers = tag->getString("ciphers"); if (!ciphers.empty()) @@ -455,7 +460,7 @@ namespace OpenSSL const std::string& GetName() const { return name; } SSL* CreateServerSession() { return ctx.CreateServerSession(); } SSL* CreateClientSession() { return clientctx.CreateClientSession(); } - const EVP_MD* GetDigest() { return digest; } + const std::vector GetDigests() { return digests; } bool AllowRenegotiation() const { return allowrenego; } unsigned int GetOutgoingRecordSize() const { return outrecsize; } }; @@ -613,13 +618,16 @@ private: GetDNString(X509_get_subject_name(cert), certinfo->dn); GetDNString(X509_get_issuer_name(cert), certinfo->issuer); - if (!X509_digest(cert, GetProfile().GetDigest(), md, &n)) - { - certinfo->error = "Out of memory generating fingerprint"; - } - else + for (const auto* digest : GetProfile().GetDigests()) { - certinfo->fingerprint = Hex::Encode(md, n); + if (!X509_digest(cert, digest, md, &n)) + { + certinfo->error = "Out of memory generating fingerprint"; + } + else + { + certinfo->fingerprints.push_back(Hex::Encode(md, n)); + } } certinfo->activation = GetTime(X509_getm_notBefore(cert)); diff --git a/src/modules/m_gateway.cpp b/src/modules/m_gateway.cpp index 33e2f10f7..78313dc68 100644 --- a/src/modules/m_gateway.cpp +++ b/src/modules/m_gateway.cpp @@ -107,9 +107,24 @@ public: return false; // Does the user have a valid fingerprint? - const std::string fp = sslapi ? sslapi->GetFingerprint(user) : ""; - if (!fingerprint.empty() && !InspIRCd::TimingSafeCompare(fp, fingerprint)) - return false; + if (!fingerprint.empty()) + { + if (!sslapi) + return false; + + bool okay = false; + for (const auto& fp : sslapi->GetFingerprints(user)) + { + if (InspIRCd::TimingSafeCompare(fp, fingerprint)) + { + okay = true; + break; + } + } + + if (!okay) + return false; + } for (const auto& mask : hostmasks) { diff --git a/src/modules/m_sasl.cpp b/src/modules/m_sasl.cpp index ac2b4dd4e..61f529643 100644 --- a/src/modules/m_sasl.cpp +++ b/src/modules/m_sasl.cpp @@ -200,9 +200,11 @@ public: std::vector params; params.push_back(method); - const std::string fp = sslapi ? sslapi->GetFingerprint(user) : ""; - if (!fp.empty()) - params.push_back(fp); + if (sslapi) + { + for (const auto& fingerprint : sslapi->GetFingerprints(user)) + params.push_back(fingerprint); + } SendSASL(user, "*", 'S', params); } diff --git a/src/modules/m_spanningtree/compat.cpp b/src/modules/m_spanningtree/compat.cpp index 917b1e480..5d61272ea 100644 --- a/src/modules/m_spanningtree/compat.cpp +++ b/src/modules/m_spanningtree/compat.cpp @@ -71,6 +71,35 @@ void TreeSocket::WriteLine(const std::string& original_line) // FRHOST was introduced in PROTO_INSPIRCD_4; drop it. return; } + else if (irc::equals(command, "METADATA")) + { + // : METADATA : + size_t targetend = NextToken(line, cmdend); + size_t nameend = NextToken(line, targetend); + size_t flagend = NextToken(line, nameend); + if (flagend != std::string::npos) + { + std::string extname(line, targetend + 1, nameend - targetend - 1); + if (irc::equals(extname, "ssl_cert")) + { + // Check we have the "e" flag (no error). + if (line.find('e', nameend + 1) < flagend) + { + size_t fpend = NextToken(line, flagend); + if (fpend != std::string::npos) + { + size_t commapos = line.find(',', flagend + 1); + if (commapos < fpend) + { + // Multiple fingerprints in ssl_cert was introduced in PROTO_INSPIRCD_4; drop it. + line.erase(commapos, fpend - commapos); + } + + } + } + } + } + } else if (irc::equals(command, "SQUERY")) { // SQUERY was introduced in PROTO_INSPIRCD_4; convert to PRIVMSG. diff --git a/src/modules/m_sslinfo.cpp b/src/modules/m_sslinfo.cpp index d6454e1c6..9c370c4ee 100644 --- a/src/modules/m_sslinfo.cpp +++ b/src/modules/m_sslinfo.cpp @@ -83,7 +83,7 @@ public: << " "; if (cert->GetError().empty()) - value << cert->GetFingerprint() << " " << cert->GetDN() << " " << cert->GetIssuer(); + value << insp::join(cert->GetFingerprints(), ',') << " " << cert->GetDN() << " " << cert->GetIssuer(); else value << cert->GetError(); @@ -117,7 +117,12 @@ public: } else { - getline(s, cert->fingerprint, ' '); + std::string fingerprints; + getline(s, fingerprints, ' '); + irc::commasepstream fingerprintstream(fingerprints); + for (std::string fingerprint; fingerprintstream.GetToken(fingerprint); ) + cert->fingerprints.push_back(fingerprint); + getline(s, cert->dn, ' '); getline(s, cert->issuer, '\n'); } @@ -211,7 +216,8 @@ private: { source->WriteNotice("*** Distinguished Name: " + cert->GetDN()); source->WriteNotice("*** Issuer: " + cert->GetIssuer()); - source->WriteNotice("*** Key Fingerprint: " + cert->GetFingerprint()); + for (const auto& fingerprint : cert->GetFingerprints()) + source->WriteNotice("*** Key Fingerprint: " + fingerprint); } } @@ -306,14 +312,23 @@ class ModuleSSLInfo final { private: CommandSSLInfo cmd; - std::string hash; - bool spkifp; + std::vector hashes; unsigned long warnexpiring; bool welcomemsg; - static bool MatchFP(ssl_cert* const cert, const std::string& fp) + static bool MatchFingerprint(const ssl_cert* cert, const std::string& fp) { - return irc::spacesepstream(fp).Contains(cert->GetFingerprint()); + irc::spacesepstream configfpstream(fp); + for (std::string configfp; configfpstream.GetToken(configfp); ) + { + for (const auto& certfp : cert->GetFingerprints()) + { + if (InspIRCd::TimingSafeCompare(certfp, configfp)) + return true; + } + } + + return false; } public: @@ -331,10 +346,19 @@ public: const auto& tag = ServerInstance->Config->ConfValue("sslinfo"); cmd.operonlyfp = tag->getBool("operonly"); cmd.sslapi.localsecure = tag->getBool("localsecure", true); - hash = tag->getString("hash"); - spkifp = tag->getBool("spkifp"); warnexpiring = tag->getDuration("warnexpiring", 0, 0, 60*60*24*365); welcomemsg = tag->getBool("welcomemsg"); + + hashes.clear(); + irc::spacesepstream hashstream(tag->getString("hash")); + for (std::string hash; hashstream.GetToken(hash); ) + { + if (!hash.compare(0, 5, "spki-", 5)) + hash.insert(4, "fp"); // spki-foo => spkifp-foo + else + hash.insert(0, "certfp-"); // foo => certfp-foo + hashes.push_back(hash); + } } void OnWhois(Whois::Context& whois) override @@ -345,8 +369,11 @@ public: ssl_cert* cert = cmd.sslapi.GetCertificate(whois.GetTarget()); if (cert) { - if ((!cmd.operonlyfp || whois.IsSelfWhois() || whois.GetSource()->IsOper()) && !cert->fingerprint.empty()) - whois.SendLine(RPL_WHOISCERTFP, INSP_FORMAT("has TLS client certificate fingerprint {}", cert->fingerprint)); + if (!cmd.operonlyfp || whois.IsSelfWhois() || whois.GetSource()->IsOper()) + { + for (const auto& fingerprint : cert->GetFingerprints()) + whois.SendLine(RPL_WHOISCERTFP, INSP_FORMAT("has TLS client certificate fingerprint {}", fingerprint)); + } } } @@ -377,7 +404,7 @@ public: } const std::string fingerprint = oper->GetConfig()->getString("fingerprint"); - if (!fingerprint.empty() && (!cert || !MatchFP(cert, fingerprint))) + if (!fingerprint.empty() && (!cert || !MatchFingerprint(cert, fingerprint))) { if (!automatic) { @@ -479,24 +506,24 @@ public: // Create a fake ssl_cert for the user. auto* cert = new ssl_cert(); - if (!hash.empty()) + cert->dn = "(unknown)"; + cert->invalid = false; + cert->issuer = "(unknown)"; + cert->trusted = true; + cert->unknownsigner = false; + for (const auto& hash : hashes) { - iter = flags->find(spkifp ? "spkifp-" : "certfp-" + hash); + iter = flags->find(hash); if (iter != flags->end() && !iter->second.empty()) { // If the gateway specifies this flag we put all trust onto them // for having validated the client certificate. This is probably // ill-advised but there's not much else we can do. - cert->fingerprint = iter->second; - cert->dn = "(unknown)"; - cert->invalid = false; - cert->issuer = "(unknown)"; - cert->trusted = true; - cert->unknownsigner = false; + cert->fingerprints.push_back(iter->second); } } - if (cert->fingerprint.empty()) + if (cert->GetFingerprints().empty()) { cert->error = "WebIRC gateway did not send a client fingerprint"; cert->revoked = true; diff --git a/src/modules/m_sslmodes.cpp b/src/modules/m_sslmodes.cpp index d5037b195..ff84b5845 100644 --- a/src/modules/m_sslmodes.cpp +++ b/src/modules/m_sslmodes.cpp @@ -56,8 +56,15 @@ public: bool IsMatch(User* user, Channel* channel, const std::string& text) override { - const std::string fp = sslapi ? sslapi->GetFingerprint(user) : ""; - return !fp.empty() && InspIRCd::Match(fp, text); + if (!sslapi) + return false; + + for (const auto& fingerprint : sslapi->GetFingerprints(user)) + { + if (InspIRCd::Match(fingerprint, text)) + return true; + } + return false; } }; -- cgit v1.3.1-10-gc9f91