File indexing completed on 2024-09-15 03:38:55
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(QString::fromLatin1(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 const auto ignoredErrors = rule.ignoredErrors(); 0097 for (QSslError::SslError e : ignoredErrors) { 0098 sl.append(d->sslErrorToString.value(e)); 0099 } 0100 } 0101 0102 if (!group.hasKey("CertificatePEM")) { 0103 group.writeEntry("CertificatePEM", rule.certificate().toPem()); 0104 } 0105 #ifdef PARANOIA 0106 else if (group.readEntry("CertificatePEM") != rule.certificate().toPem()) { 0107 return; 0108 } 0109 #endif 0110 group.writeEntry(rule.hostName(), sl); 0111 group.sync(); 0112 } 0113 0114 void KSSLD::clearRule(const KSslCertificateRule &rule) 0115 { 0116 clearRule(rule.certificate(), rule.hostName()); 0117 } 0118 0119 void KSSLD::clearRule(const QSslCertificate &cert, const QString &hostName) 0120 { 0121 KConfigGroup group = d->config.group(QString::fromLatin1(cert.digest().toHex())); 0122 group.deleteEntry(hostName); 0123 if (group.keyList().size() < 2) { 0124 group.deleteGroup(); 0125 } 0126 group.sync(); 0127 } 0128 0129 void KSSLD::pruneExpiredRules() 0130 { 0131 // expired rules are deleted when trying to load them, so we just try to load all rules. 0132 // be careful about iterating over KConfig(Group) while changing it 0133 const QStringList groupNames = d->config.groupList(); 0134 for (const QString &groupName : groupNames) { 0135 QByteArray certDigest = groupName.toLatin1(); 0136 const QStringList keys = d->config.group(groupName).keyList(); 0137 for (const QString &key : keys) { 0138 if (key == QLatin1String("CertificatePEM")) { 0139 continue; 0140 } 0141 KSslCertificateRule r = rule(QSslCertificate(certDigest), key); 0142 } 0143 } 0144 } 0145 0146 // check a domain name with subdomains for well-formedness and count the dot-separated parts 0147 static QString normalizeSubdomains(const QString &hostName, int *namePartsCount) 0148 { 0149 QString ret; 0150 int partsCount = 0; 0151 bool wasPrevDot = true; // -> allow no dot at the beginning and count first name part 0152 const int length = hostName.length(); 0153 for (int i = 0; i < length; i++) { 0154 const QChar c = hostName.at(i); 0155 if (c == QLatin1Char('.')) { 0156 if (wasPrevDot || (i + 1 == hostName.length())) { 0157 // consecutive dots or a dot at the end are forbidden 0158 partsCount = 0; 0159 ret.clear(); 0160 break; 0161 } 0162 wasPrevDot = true; 0163 } else { 0164 if (wasPrevDot) { 0165 partsCount++; 0166 } 0167 wasPrevDot = false; 0168 } 0169 ret.append(c); 0170 } 0171 0172 *namePartsCount = partsCount; 0173 return ret; 0174 } 0175 0176 KSslCertificateRule KSSLD::rule(const QSslCertificate &cert, const QString &hostName) const 0177 { 0178 const QByteArray certDigest = cert.digest().toHex(); 0179 KConfigGroup group = d->config.group(QString::fromLatin1(certDigest)); 0180 0181 KSslCertificateRule ret(cert, hostName); 0182 bool foundHostName = false; 0183 0184 int needlePartsCount; 0185 QString needle = normalizeSubdomains(hostName, &needlePartsCount); 0186 0187 // Find a rule for the hostname, either... 0188 if (group.hasKey(needle)) { 0189 // directly (host, site.tld, a.site.tld etc) 0190 if (needlePartsCount >= 1) { 0191 foundHostName = true; 0192 } 0193 } else { 0194 // or with wildcards 0195 // "tld" <- "*." and "site.tld" <- "*.tld" are not valid matches, 0196 // "a.site.tld" <- "*.site.tld" is 0197 while (--needlePartsCount >= 2) { 0198 const int dotIndex = needle.indexOf(QLatin1Char('.')); 0199 Q_ASSERT(dotIndex > 0); // if this fails normalizeSubdomains() failed 0200 needle.remove(0, dotIndex - 1); 0201 needle[0] = QChar::fromLatin1('*'); 0202 if (group.hasKey(needle)) { 0203 foundHostName = true; 0204 break; 0205 } 0206 needle.remove(0, 2); // remove "*." 0207 } 0208 } 0209 0210 if (!foundHostName) { 0211 // Don't make a rule with the failed wildcard pattern - use the original hostname. 0212 return KSslCertificateRule(cert, hostName); 0213 } 0214 0215 // parse entry of the format "ExpireUTC <date>, Reject" or 0216 //"ExpireUTC <date>, HostNameMismatch, ExpiredCertificate, ..." 0217 QStringList sl = group.readEntry(needle, QStringList()); 0218 0219 QDateTime expiryDt; 0220 // the rule is well-formed if it contains at least the expire date and one directive 0221 if (sl.size() >= 2) { 0222 QString dtString = sl.takeFirst(); 0223 if (dtString.startsWith(QLatin1String("ExpireUTC "))) { 0224 dtString.remove(0, 10 /* length of "ExpireUTC " */); 0225 expiryDt = QDateTime::fromString(dtString, Qt::ISODate); 0226 } 0227 } 0228 0229 if (!expiryDt.isValid() || expiryDt < QDateTime::currentDateTime()) { 0230 // the entry is malformed or expired so we remove it 0231 group.deleteEntry(needle); 0232 // the group is useless once only the CertificatePEM entry left 0233 if (group.keyList().size() < 2) { 0234 group.deleteGroup(); 0235 } 0236 return ret; 0237 } 0238 0239 QList<QSslError::SslError> ignoredErrors; 0240 bool isRejected = false; 0241 for (const QString &s : std::as_const(sl)) { 0242 if (s == QLatin1String("Reject")) { 0243 isRejected = true; 0244 ignoredErrors.clear(); 0245 break; 0246 } 0247 if (!d->stringToSslError.contains(s)) { 0248 continue; 0249 } 0250 ignoredErrors.append(d->stringToSslError.value(s)); 0251 } 0252 0253 // Everything is checked and we can make ret valid 0254 ret.setExpiryDateTime(expiryDt); 0255 ret.setRejected(isRejected); 0256 ret.setIgnoredErrors(ignoredErrors); 0257 return ret; 0258 } 0259 0260 #include "kssld.moc" 0261 #include "moc_kssld.cpp" 0262 #include "moc_kssld_adaptor.cpp"