File indexing completed on 2024-04-28 07:44:26

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"