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[] = {{.str: "NoError", .err: QSslError::NoError},
36 {.str: "UnknownError", .err: QSslError::UnspecifiedError},
37 {.str: "InvalidCertificateAuthority", .err: QSslError::InvalidCaCertificate},
38 {.str: "InvalidCertificate", .err: QSslError::UnableToDecodeIssuerPublicKey},
39 {.str: "CertificateSignatureFailed", .err: QSslError::CertificateSignatureFailed},
40 {.str: "SelfSignedCertificate", .err: QSslError::SelfSignedCertificate},
41 {.str: "RevokedCertificate", .err: QSslError::CertificateRevoked},
42 {.str: "InvalidCertificatePurpose", .err: QSslError::InvalidPurpose},
43 {.str: "RejectedCertificate", .err: QSslError::CertificateRejected},
44 {.str: "UntrustedCertificate", .err: QSslError::CertificateUntrusted},
45 {.str: "ExpiredCertificate", .err: QSslError::CertificateExpired},
46 {.str: "HostNameMismatch", .err: QSslError::HostNameMismatch},
47 {.str: "UnableToGetLocalIssuerCertificate", .err: QSslError::UnableToGetLocalIssuerCertificate},
48 {.str: "InvalidNotBeforeField", .err: QSslError::InvalidNotBeforeField},
49 {.str: "InvalidNotAfterField", .err: QSslError::InvalidNotAfterField},
50 {.str: "CertificateNotYetValid", .err: QSslError::CertificateNotYetValid},
51 {.str: "SubjectIssuerMismatch", .err: QSslError::SubjectIssuerMismatch},
52 {.str: "AuthorityIssuerSerialNumberMismatch", .err: QSslError::AuthorityIssuerSerialNumberMismatch},
53 {.str: "SelfSignedCertificateInChain", .err: QSslError::SelfSignedCertificateInChain},
54 {.str: "UnableToVerifyFirstCertificate", .err: QSslError::UnableToVerifyFirstCertificate},
55 {.str: "UnableToDecryptCertificateSignature", .err: QSslError::UnableToDecryptCertificateSignature},
56 {.str: "UnableToGetIssuerCertificate", .err: QSslError::UnableToGetIssuerCertificate}};
57
58 for (const strErr &row : strError) {
59 QString s = QString::fromLatin1(ba: row.str);
60 stringToSslError.insert(key: s, value: row.err);
61 sslErrorToString.insert(key: row.err, value: s);
62 }
63 }
64
65 KConfig config;
66 QHash<QString, QSslError::SslError> stringToSslError;
67 QHash<QSslError::SslError, QString> sslErrorToString;
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(group: QString::fromLatin1(ba: rule.certificate().digest().toHex()));
86
87 QStringList sl;
88
89 QString dtString = QStringLiteral("ExpireUTC ");
90 dtString.append(s: rule.expiryDateTime().toString(format: Qt::ISODate));
91 sl.append(t: 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(t: d->sslErrorToString.value(key: e));
99 }
100 }
101
102 if (!group.hasKey(key: "CertificatePEM")) {
103 group.writeEntry(key: "CertificatePEM", value: rule.certificate().toPem());
104 }
105#ifdef PARANOIA
106 else if (group.readEntry("CertificatePEM") != rule.certificate().toPem()) {
107 return;
108 }
109#endif
110 group.writeEntry(key: rule.hostName(), value: sl);
111 group.sync();
112}
113
114void KSSLD::clearRule(const KSslCertificateRule &rule)
115{
116 clearRule(cert: rule.certificate(), hostName: rule.hostName());
117}
118
119void KSSLD::clearRule(const QSslCertificate &cert, const QString &hostName)
120{
121 KConfigGroup group = d->config.group(group: QString::fromLatin1(ba: cert.digest().toHex()));
122 group.deleteEntry(pKey: 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(group: groupName).keyList();
137 for (const QString &key : keys) {
138 if (key == QLatin1String("CertificatePEM")) {
139 continue;
140 }
141 KSslCertificateRule r = rule(cert: QSslCertificate(certDigest), hostName: 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(group: QString::fromLatin1(ba: certDigest));
180
181 KSslCertificateRule ret(cert, hostName);
182 bool foundHostName = false;
183
184 int needlePartsCount;
185 QString needle = normalizeSubdomains(hostName, namePartsCount: &needlePartsCount);
186
187 // Find a rule for the hostname, either...
188 if (group.hasKey(key: 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(c: QLatin1Char('.'));
199 Q_ASSERT(dotIndex > 0); // if this fails normalizeSubdomains() failed
200 needle.remove(i: 0, len: dotIndex - 1);
201 needle[0] = QChar::fromLatin1(c: '*');
202 if (group.hasKey(key: needle)) {
203 foundHostName = true;
204 break;
205 }
206 needle.remove(i: 0, len: 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(key: needle, aDefault: 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(s: QLatin1String("ExpireUTC "))) {
224 dtString.remove(i: 0, len: 10 /* length of "ExpireUTC " */);
225 expiryDt = QDateTime::fromString(string: dtString, format: 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(pKey: 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(t&: sl)) {
242 if (s == QLatin1String("Reject")) {
243 isRejected = true;
244 ignoredErrors.clear();
245 break;
246 }
247 if (!d->stringToSslError.contains(key: s)) {
248 continue;
249 }
250 ignoredErrors.append(t: d->stringToSslError.value(key: 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"
263

source code of kio/src/kssld/kssld.cpp