File indexing completed on 2024-04-21 11:33:22

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"