KIO

kssld.cpp
1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2007, 2008, 2010 Andreas Hartmetz <ahartmetz@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "kssld.h"
9
10#include "ksslcertificatemanager.h"
11#include "ksslcertificatemanager_p.h"
12#include "kssld_adaptor.h"
13
14#include <KConfig>
15#include <KConfigGroup>
16
17#include <KPluginFactory>
18#include <QDate>
19
20K_PLUGIN_CLASS_WITH_JSON(KSSLD, "kssld.json")
21
22class KSSLDPrivate
23{
24public:
25 KSSLDPrivate()
26 : config(QStringLiteral("ksslcertificatemanager"), KConfig::SimpleConfig)
27 {
28 struct strErr {
29 const char *str;
30 QSslError::SslError err;
31 };
32
33 // hmmm, looks like these are all of the errors where it is possible to continue.
34 // TODO for Qt > 5.14 QSslError::SslError is a Q_ENUM, and we can therefore replace this manual mapping table
35 const static strErr strError[] = {{"NoError", QSslError::NoError},
36 {"UnknownError", QSslError::UnspecifiedError},
37 {"InvalidCertificateAuthority", QSslError::InvalidCaCertificate},
38 {"InvalidCertificate", QSslError::UnableToDecodeIssuerPublicKey},
39 {"CertificateSignatureFailed", QSslError::CertificateSignatureFailed},
40 {"SelfSignedCertificate", QSslError::SelfSignedCertificate},
41 {"RevokedCertificate", QSslError::CertificateRevoked},
42 {"InvalidCertificatePurpose", QSslError::InvalidPurpose},
43 {"RejectedCertificate", QSslError::CertificateRejected},
44 {"UntrustedCertificate", QSslError::CertificateUntrusted},
45 {"ExpiredCertificate", QSslError::CertificateExpired},
46 {"HostNameMismatch", QSslError::HostNameMismatch},
47 {"UnableToGetLocalIssuerCertificate", QSslError::UnableToGetLocalIssuerCertificate},
48 {"InvalidNotBeforeField", QSslError::InvalidNotBeforeField},
49 {"InvalidNotAfterField", QSslError::InvalidNotAfterField},
50 {"CertificateNotYetValid", QSslError::CertificateNotYetValid},
51 {"SubjectIssuerMismatch", QSslError::SubjectIssuerMismatch},
52 {"AuthorityIssuerSerialNumberMismatch", QSslError::AuthorityIssuerSerialNumberMismatch},
53 {"SelfSignedCertificateInChain", QSslError::SelfSignedCertificateInChain},
54 {"UnableToVerifyFirstCertificate", QSslError::UnableToVerifyFirstCertificate},
55 {"UnableToDecryptCertificateSignature", QSslError::UnableToDecryptCertificateSignature},
56 {"UnableToGetIssuerCertificate", QSslError::UnableToGetIssuerCertificate}};
57
58 for (const strErr &row : strError) {
59 QString s = QString::fromLatin1(row.str);
60 stringToSslError.insert(s, row.err);
61 sslErrorToString.insert(row.err, s);
62 }
63 }
64
65 KConfig config;
68};
69
70KSSLD::KSSLD(QObject *parent, const QVariantList &)
71 : KDEDModule(parent)
72 , d(new KSSLDPrivate())
73{
74 new KSSLDAdaptor(this);
75 pruneExpiredRules();
76}
77
78KSSLD::~KSSLD() = default;
79
80void KSSLD::setRule(const KSslCertificateRule &rule)
81{
82 if (rule.hostName().isEmpty()) {
83 return;
84 }
85 KConfigGroup group = d->config.group(QString::fromLatin1(rule.certificate().digest().toHex()));
86
87 QStringList sl;
88
89 QString dtString = QStringLiteral("ExpireUTC ");
90 dtString.append(rule.expiryDateTime().toString(Qt::ISODate));
91 sl.append(dtString);
92
93 if (rule.isRejected()) {
94 sl.append(QStringLiteral("Reject"));
95 } else {
96 const auto ignoredErrors = rule.ignoredErrors();
97 for (QSslError::SslError e : ignoredErrors) {
98 sl.append(d->sslErrorToString.value(e));
99 }
100 }
101
102 if (!group.hasKey("CertificatePEM")) {
103 group.writeEntry("CertificatePEM", rule.certificate().toPem());
104 }
105#ifdef PARANOIA
106 else if (group.readEntry("CertificatePEM") != rule.certificate().toPem()) {
107 return;
108 }
109#endif
110 group.writeEntry(rule.hostName(), sl);
111 group.sync();
112}
113
114void KSSLD::clearRule(const KSslCertificateRule &rule)
115{
116 clearRule(rule.certificate(), rule.hostName());
117}
118
119void KSSLD::clearRule(const QSslCertificate &cert, const QString &hostName)
120{
121 KConfigGroup group = d->config.group(QString::fromLatin1(cert.digest().toHex()));
122 group.deleteEntry(hostName);
123 if (group.keyList().size() < 2) {
124 group.deleteGroup();
125 }
126 group.sync();
127}
128
129void KSSLD::pruneExpiredRules()
130{
131 // expired rules are deleted when trying to load them, so we just try to load all rules.
132 // be careful about iterating over KConfig(Group) while changing it
133 const QStringList groupNames = d->config.groupList();
134 for (const QString &groupName : groupNames) {
135 QByteArray certDigest = groupName.toLatin1();
136 const QStringList keys = d->config.group(groupName).keyList();
137 for (const QString &key : keys) {
138 if (key == QLatin1String("CertificatePEM")) {
139 continue;
140 }
141 KSslCertificateRule r = rule(QSslCertificate(certDigest), key);
142 }
143 }
144}
145
146// check a domain name with subdomains for well-formedness and count the dot-separated parts
147static QString normalizeSubdomains(const QString &hostName, int *namePartsCount)
148{
149 QString ret;
150 int partsCount = 0;
151 bool wasPrevDot = true; // -> allow no dot at the beginning and count first name part
152 const int length = hostName.length();
153 for (int i = 0; i < length; i++) {
154 const QChar c = hostName.at(i);
155 if (c == QLatin1Char('.')) {
156 if (wasPrevDot || (i + 1 == hostName.length())) {
157 // consecutive dots or a dot at the end are forbidden
158 partsCount = 0;
159 ret.clear();
160 break;
161 }
162 wasPrevDot = true;
163 } else {
164 if (wasPrevDot) {
165 partsCount++;
166 }
167 wasPrevDot = false;
168 }
169 ret.append(c);
170 }
171
172 *namePartsCount = partsCount;
173 return ret;
174}
175
176KSslCertificateRule KSSLD::rule(const QSslCertificate &cert, const QString &hostName) const
177{
178 const QByteArray certDigest = cert.digest().toHex();
179 KConfigGroup group = d->config.group(QString::fromLatin1(certDigest));
180
181 KSslCertificateRule ret(cert, hostName);
182 bool foundHostName = false;
183
184 int needlePartsCount;
185 QString needle = normalizeSubdomains(hostName, &needlePartsCount);
186
187 // Find a rule for the hostname, either...
188 if (group.hasKey(needle)) {
189 // directly (host, site.tld, a.site.tld etc)
190 if (needlePartsCount >= 1) {
191 foundHostName = true;
192 }
193 } else {
194 // or with wildcards
195 // "tld" <- "*." and "site.tld" <- "*.tld" are not valid matches,
196 // "a.site.tld" <- "*.site.tld" is
197 while (--needlePartsCount >= 2) {
198 const int dotIndex = needle.indexOf(QLatin1Char('.'));
199 Q_ASSERT(dotIndex > 0); // if this fails normalizeSubdomains() failed
200 needle.remove(0, dotIndex - 1);
201 needle[0] = QChar::fromLatin1('*');
202 if (group.hasKey(needle)) {
203 foundHostName = true;
204 break;
205 }
206 needle.remove(0, 2); // remove "*."
207 }
208 }
209
210 if (!foundHostName) {
211 // Don't make a rule with the failed wildcard pattern - use the original hostname.
212 return KSslCertificateRule(cert, hostName);
213 }
214
215 // parse entry of the format "ExpireUTC <date>, Reject" or
216 //"ExpireUTC <date>, HostNameMismatch, ExpiredCertificate, ..."
217 QStringList sl = group.readEntry(needle, QStringList());
218
219 QDateTime expiryDt;
220 // the rule is well-formed if it contains at least the expire date and one directive
221 if (sl.size() >= 2) {
222 QString dtString = sl.takeFirst();
223 if (dtString.startsWith(QLatin1String("ExpireUTC "))) {
224 dtString.remove(0, 10 /* length of "ExpireUTC " */);
225 expiryDt = QDateTime::fromString(dtString, Qt::ISODate);
226 }
227 }
228
229 if (!expiryDt.isValid() || expiryDt < QDateTime::currentDateTime()) {
230 // the entry is malformed or expired so we remove it
231 group.deleteEntry(needle);
232 // the group is useless once only the CertificatePEM entry left
233 if (group.keyList().size() < 2) {
234 group.deleteGroup();
235 }
236 return ret;
237 }
238
239 QList<QSslError::SslError> ignoredErrors;
240 bool isRejected = false;
241 for (const QString &s : std::as_const(sl)) {
242 if (s == QLatin1String("Reject")) {
243 isRejected = true;
244 ignoredErrors.clear();
245 break;
246 }
247 if (!d->stringToSslError.contains(s)) {
248 continue;
249 }
250 ignoredErrors.append(d->stringToSslError.value(s));
251 }
252
253 // Everything is checked and we can make ret valid
254 ret.setExpiryDateTime(expiryDt);
255 ret.setRejected(isRejected);
256 ret.setIgnoredErrors(ignoredErrors);
257 return ret;
258}
259
260#include "kssld.moc"
261#include "moc_kssld.cpp"
262#include "moc_kssld_adaptor.cpp"
void deleteEntry(const char *key, WriteConfigFlags pFlags=Normal)
bool hasKey(const char *key) const
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
void deleteGroup(const QString &group, WriteConfigFlags flags=Normal)
QString readEntry(const char *key, const char *aDefault=nullptr) const
bool sync() override
QStringList keyList() const
#define K_PLUGIN_CLASS_WITH_JSON(classname, jsonFile)
QByteArray toHex(char separator) const const
QChar fromLatin1(char c)
QDateTime currentDateTime()
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid() const const
QString toString(QStringView format, QCalendar cal) const const
void append(QList< T > &&value)
void clear()
qsizetype size() const const
value_type takeFirst()
QByteArray digest(QCryptographicHash::Algorithm algorithm) const const
QByteArray toPem() const const
QString & append(QChar ch)
const QChar at(qsizetype position) const const
void clear()
QString fromLatin1(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype length() const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:18:52 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.