File indexing completed on 2023-09-24 04:09:14
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2007, 2008, 2010 Andreas Hartmetz <ahartmetz@gmail.com> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "kssld.h" 0009 0010 #include "ksslcertificatemanager.h" 0011 #include "ksslcertificatemanager_p.h" 0012 #include "kssld_adaptor.h" 0013 0014 #include <KConfig> 0015 #include <KConfigGroup> 0016 0017 #include <KPluginFactory> 0018 #include <QDate> 0019 0020 K_PLUGIN_CLASS_WITH_JSON(KSSLD, "kssld.json") 0021 0022 class KSSLDPrivate 0023 { 0024 public: 0025 KSSLDPrivate() 0026 : config(QStringLiteral("ksslcertificatemanager"), KConfig::SimpleConfig) 0027 { 0028 struct strErr { 0029 const char *str; 0030 QSslError::SslError err; 0031 }; 0032 0033 // hmmm, looks like these are all of the errors where it is possible to continue. 0034 // TODO for Qt > 5.14 QSslError::SslError is a Q_ENUM, and we can therefore replace this manual mapping table 0035 const static strErr strError[] = {{"NoError", QSslError::NoError}, 0036 {"UnknownError", QSslError::UnspecifiedError}, 0037 {"InvalidCertificateAuthority", QSslError::InvalidCaCertificate}, 0038 {"InvalidCertificate", QSslError::UnableToDecodeIssuerPublicKey}, 0039 {"CertificateSignatureFailed", QSslError::CertificateSignatureFailed}, 0040 {"SelfSignedCertificate", QSslError::SelfSignedCertificate}, 0041 {"RevokedCertificate", QSslError::CertificateRevoked}, 0042 {"InvalidCertificatePurpose", QSslError::InvalidPurpose}, 0043 {"RejectedCertificate", QSslError::CertificateRejected}, 0044 {"UntrustedCertificate", QSslError::CertificateUntrusted}, 0045 {"ExpiredCertificate", QSslError::CertificateExpired}, 0046 {"HostNameMismatch", QSslError::HostNameMismatch}, 0047 {"UnableToGetLocalIssuerCertificate", QSslError::UnableToGetLocalIssuerCertificate}, 0048 {"InvalidNotBeforeField", QSslError::InvalidNotBeforeField}, 0049 {"InvalidNotAfterField", QSslError::InvalidNotAfterField}, 0050 {"CertificateNotYetValid", QSslError::CertificateNotYetValid}, 0051 {"SubjectIssuerMismatch", QSslError::SubjectIssuerMismatch}, 0052 {"AuthorityIssuerSerialNumberMismatch", QSslError::AuthorityIssuerSerialNumberMismatch}, 0053 {"SelfSignedCertificateInChain", QSslError::SelfSignedCertificateInChain}, 0054 {"UnableToVerifyFirstCertificate", QSslError::UnableToVerifyFirstCertificate}, 0055 {"UnableToDecryptCertificateSignature", QSslError::UnableToDecryptCertificateSignature}, 0056 {"UnableToGetIssuerCertificate", QSslError::UnableToGetIssuerCertificate}}; 0057 0058 for (const strErr &row : strError) { 0059 QString s = QString::fromLatin1(row.str); 0060 stringToSslError.insert(s, row.err); 0061 sslErrorToString.insert(row.err, s); 0062 } 0063 } 0064 0065 KConfig config; 0066 QHash<QString, QSslError::SslError> stringToSslError; 0067 QHash<QSslError::SslError, QString> sslErrorToString; 0068 }; 0069 0070 KSSLD::KSSLD(QObject *parent, const QVariantList &) 0071 : KDEDModule(parent) 0072 , d(new KSSLDPrivate()) 0073 { 0074 new KSSLDAdaptor(this); 0075 pruneExpiredRules(); 0076 } 0077 0078 KSSLD::~KSSLD() = default; 0079 0080 void KSSLD::setRule(const KSslCertificateRule &rule) 0081 { 0082 if (rule.hostName().isEmpty()) { 0083 return; 0084 } 0085 KConfigGroup group = d->config.group(rule.certificate().digest().toHex()); 0086 0087 QStringList sl; 0088 0089 QString dtString = QStringLiteral("ExpireUTC "); 0090 dtString.append(rule.expiryDateTime().toString(Qt::ISODate)); 0091 sl.append(dtString); 0092 0093 if (rule.isRejected()) { 0094 sl.append(QStringLiteral("Reject")); 0095 } else { 0096 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 64) 0097 for (QSslError::SslError e : std::as_const(rule.d->ignoredErrors)) { 0098 #else 0099 const auto ignoredErrors = rule.ignoredErrors(); 0100 for (QSslError::SslError e : ignoredErrors) { 0101 #endif 0102 sl.append(d->sslErrorToString.value(e)); 0103 } 0104 } 0105 0106 if (!group.hasKey("CertificatePEM")) { 0107 group.writeEntry("CertificatePEM", rule.certificate().toPem()); 0108 } 0109 #ifdef PARANOIA 0110 else if (group.readEntry("CertificatePEM") != rule.certificate().toPem()) { 0111 return; 0112 } 0113 #endif 0114 group.writeEntry(rule.hostName(), sl); 0115 group.sync(); 0116 } 0117 0118 void KSSLD::clearRule(const KSslCertificateRule &rule) 0119 { 0120 clearRule(rule.certificate(), rule.hostName()); 0121 } 0122 0123 void KSSLD::clearRule(const QSslCertificate &cert, const QString &hostName) 0124 { 0125 KConfigGroup group = d->config.group(cert.digest().toHex()); 0126 group.deleteEntry(hostName); 0127 if (group.keyList().size() < 2) { 0128 group.deleteGroup(); 0129 } 0130 group.sync(); 0131 } 0132 0133 void KSSLD::pruneExpiredRules() 0134 { 0135 // expired rules are deleted when trying to load them, so we just try to load all rules. 0136 // be careful about iterating over KConfig(Group) while changing it 0137 const QStringList groupNames = d->config.groupList(); 0138 for (const QString &groupName : groupNames) { 0139 QByteArray certDigest = groupName.toLatin1(); 0140 const QStringList keys = d->config.group(groupName).keyList(); 0141 for (const QString &key : keys) { 0142 if (key == QLatin1String("CertificatePEM")) { 0143 continue; 0144 } 0145 KSslCertificateRule r = rule(QSslCertificate(certDigest), key); 0146 } 0147 } 0148 } 0149 0150 // check a domain name with subdomains for well-formedness and count the dot-separated parts 0151 static QString normalizeSubdomains(const QString &hostName, int *namePartsCount) 0152 { 0153 QString ret; 0154 int partsCount = 0; 0155 bool wasPrevDot = true; // -> allow no dot at the beginning and count first name part 0156 const int length = hostName.length(); 0157 for (int i = 0; i < length; i++) { 0158 const QChar c = hostName.at(i); 0159 if (c == QLatin1Char('.')) { 0160 if (wasPrevDot || (i + 1 == hostName.length())) { 0161 // consecutive dots or a dot at the end are forbidden 0162 partsCount = 0; 0163 ret.clear(); 0164 break; 0165 } 0166 wasPrevDot = true; 0167 } else { 0168 if (wasPrevDot) { 0169 partsCount++; 0170 } 0171 wasPrevDot = false; 0172 } 0173 ret.append(c); 0174 } 0175 0176 *namePartsCount = partsCount; 0177 return ret; 0178 } 0179 0180 KSslCertificateRule KSSLD::rule(const QSslCertificate &cert, const QString &hostName) const 0181 { 0182 const QByteArray certDigest = cert.digest().toHex(); 0183 KConfigGroup group = d->config.group(certDigest); 0184 0185 KSslCertificateRule ret(cert, hostName); 0186 bool foundHostName = false; 0187 0188 int needlePartsCount; 0189 QString needle = normalizeSubdomains(hostName, &needlePartsCount); 0190 0191 // Find a rule for the hostname, either... 0192 if (group.hasKey(needle)) { 0193 // directly (host, site.tld, a.site.tld etc) 0194 if (needlePartsCount >= 1) { 0195 foundHostName = true; 0196 } 0197 } else { 0198 // or with wildcards 0199 // "tld" <- "*." and "site.tld" <- "*.tld" are not valid matches, 0200 // "a.site.tld" <- "*.site.tld" is 0201 while (--needlePartsCount >= 2) { 0202 const int dotIndex = needle.indexOf(QLatin1Char('.')); 0203 Q_ASSERT(dotIndex > 0); // if this fails normalizeSubdomains() failed 0204 needle.remove(0, dotIndex - 1); 0205 needle[0] = QChar::fromLatin1('*'); 0206 if (group.hasKey(needle)) { 0207 foundHostName = true; 0208 break; 0209 } 0210 needle.remove(0, 2); // remove "*." 0211 } 0212 } 0213 0214 if (!foundHostName) { 0215 // Don't make a rule with the failed wildcard pattern - use the original hostname. 0216 return KSslCertificateRule(cert, hostName); 0217 } 0218 0219 // parse entry of the format "ExpireUTC <date>, Reject" or 0220 //"ExpireUTC <date>, HostNameMismatch, ExpiredCertificate, ..." 0221 QStringList sl = group.readEntry(needle, QStringList()); 0222 0223 QDateTime expiryDt; 0224 // the rule is well-formed if it contains at least the expire date and one directive 0225 if (sl.size() >= 2) { 0226 QString dtString = sl.takeFirst(); 0227 if (dtString.startsWith(QLatin1String("ExpireUTC "))) { 0228 dtString.remove(0, 10 /* length of "ExpireUTC " */); 0229 expiryDt = QDateTime::fromString(dtString, Qt::ISODate); 0230 } 0231 } 0232 0233 if (!expiryDt.isValid() || expiryDt < QDateTime::currentDateTime()) { 0234 // the entry is malformed or expired so we remove it 0235 group.deleteEntry(needle); 0236 // the group is useless once only the CertificatePEM entry left 0237 if (group.keyList().size() < 2) { 0238 group.deleteGroup(); 0239 } 0240 return ret; 0241 } 0242 0243 QList<QSslError::SslError> ignoredErrors; 0244 bool isRejected = false; 0245 for (const QString &s : std::as_const(sl)) { 0246 if (s == QLatin1String("Reject")) { 0247 isRejected = true; 0248 ignoredErrors.clear(); 0249 break; 0250 } 0251 if (!d->stringToSslError.contains(s)) { 0252 continue; 0253 } 0254 ignoredErrors.append(d->stringToSslError.value(s)); 0255 } 0256 0257 // Everything is checked and we can make ret valid 0258 ret.setExpiryDateTime(expiryDt); 0259 ret.setRejected(isRejected); 0260 ret.setIgnoredErrors(ignoredErrors); 0261 return ret; 0262 } 0263 0264 #include "kssld.moc" 0265 #include "moc_kssld.cpp" 0266 #include "moc_kssld_adaptor.cpp"