KIO

ksslcertificatemanager.cpp
1/*
2 This file is part of the KDE project
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 "ksslcertificatemanager.h"
9#include "ksslcertificatemanager_p.h"
10
11#ifdef WITH_QTDBUS
12#include "kssld_interface.h"
13#endif
14
15#include "ksslerroruidata_p.h"
16
17#include <KConfig>
18#include <KConfigGroup>
19#include <KLocalizedString>
20
21#include <QDebug>
22#include <QDir>
23#include <QFile>
24#include <QSslConfiguration>
25#include <QStandardPaths>
26
27#include <set>
28
29/*
30 Config file format:
31[<MD5-Digest>]
32<Host> = <Date> <List of ignored errors>
33#for example
34#mail.kdab.net = ExpireUTC 2008-08-20T18:22:14, SelfSigned, Expired
35#very.old.com = ExpireUTC 2008-08-20T18:22:14, TooWeakEncryption <- not actually planned to implement
36#clueless.admin.com = ExpireUTC 2008-08-20T18:22:14, HostNameMismatch
37#
38#Wildcard syntax
39#* = ExpireUTC 2008-08-20T18:22:14, SelfSigned
40#*.kdab.net = ExpireUTC 2008-08-20T18:22:14, SelfSigned
41#mail.kdab.net = ExpireUTC 2008-08-20T18:22:14, All <- not implemented
42#* = ExpireUTC 9999-12-31T23:59:59, Reject #we know that something is wrong with that certificate
43CertificatePEM = <PEM-encoded certificate> #host entries are all lowercase, thus no clashes
44
45 */
46
47// TODO GUI for managing exception rules
48
49KSslCertificateRule::KSslCertificateRule(const QSslCertificate &cert, const QString &hostName)
50 : d(new KSslCertificateRulePrivate())
51{
52 d->certificate = cert;
53 d->hostName = hostName;
54 d->isRejected = false;
55}
56
57KSslCertificateRule::KSslCertificateRule(const KSslCertificateRule &other)
58 : d(new KSslCertificateRulePrivate())
59{
60 *d = *other.d;
61}
62
63KSslCertificateRule::~KSslCertificateRule() = default;
64
65KSslCertificateRule &KSslCertificateRule::operator=(const KSslCertificateRule &other)
66{
67 *d = *other.d;
68 return *this;
69}
70
71QSslCertificate KSslCertificateRule::certificate() const
72{
73 return d->certificate;
74}
75
76QString KSslCertificateRule::hostName() const
77{
78 return d->hostName;
79}
80
81void KSslCertificateRule::setExpiryDateTime(const QDateTime &dateTime)
82{
83 d->expiryDateTime = dateTime;
84}
85
86QDateTime KSslCertificateRule::expiryDateTime() const
87{
88 return d->expiryDateTime;
89}
90
91void KSslCertificateRule::setRejected(bool rejected)
92{
93 d->isRejected = rejected;
94}
95
96bool KSslCertificateRule::isRejected() const
97{
98 return d->isRejected;
99}
100
101bool KSslCertificateRule::isErrorIgnored(QSslError::SslError error) const
102{
103 return d->ignoredErrors.contains(error);
104}
105
107{
108 d->ignoredErrors.clear();
109 for (const QSslError &error : errors) {
110 if (!isErrorIgnored(error.error())) {
111 d->ignoredErrors.append(error.error());
112 }
113 }
114}
115
117{
118 d->ignoredErrors.clear();
119 for (QSslError::SslError error : errors) {
120 if (!isErrorIgnored(error)) {
121 d->ignoredErrors.append(error);
122 }
123 }
124}
125
126QList<QSslError::SslError> KSslCertificateRule::ignoredErrors() const
127{
128 return d->ignoredErrors;
129}
130
132{
134 for (const QSslError &error : errors) {
135 if (!isErrorIgnored(error.error())) {
136 ret.append(error);
137 }
138 }
139 return ret;
140}
141
142////////////////////////////////////////////////////////////////////
143
144static QList<QSslCertificate> deduplicate(const QList<QSslCertificate> &certs)
145{
146 std::set<QByteArray> digests;
148 for (const QSslCertificate &cert : certs) {
149 QByteArray digest = cert.digest();
150 const auto [it, isInserted] = digests.insert(digest);
151 if (isInserted) {
152 ret.append(cert);
153 }
154 }
155 return ret;
156}
157
158KSslCertificateManagerPrivate::KSslCertificateManagerPrivate()
159 : config(QStringLiteral("ksslcertificatemanager"), KConfig::SimpleConfig)
160#ifdef WITH_QTDBUS
161 , iface(new org::kde::KSSLDInterface(QStringLiteral("org.kde.kssld6"), QStringLiteral("/modules/kssld"), QDBusConnection::sessionBus()))
162#endif
163 , isCertListLoaded(false)
164 , userCertDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kssl/userCaCertificates/"))
165{
166}
167
168KSslCertificateManagerPrivate::~KSslCertificateManagerPrivate()
169{
170#ifdef WITH_QTDBUS
171 delete iface;
172 iface = nullptr;
173#endif
174}
175
176void KSslCertificateManagerPrivate::loadDefaultCaCertificates()
177{
178 defaultCaCertificates.clear();
179
180 QList<QSslCertificate> certs = deduplicate(QSslConfiguration::systemCaCertificates());
181
182 KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig);
183 KConfigGroup group = config.group(QStringLiteral("Blacklist of CA Certificates"));
184
185 certs.append(QSslCertificate::fromPath(userCertDir + QLatin1Char('*'), QSsl::Pem, QSslCertificate::PatternSyntax::Wildcard));
186
187 for (const QSslCertificate &cert : std::as_const(certs)) {
188 const QByteArray digest = cert.digest().toHex();
189 if (!group.hasKey(digest.constData())) {
190 defaultCaCertificates += cert;
191 }
192 }
193
194 isCertListLoaded = true;
195}
196
197bool KSslCertificateManagerPrivate::addCertificate(const KSslCaCertificate &in)
198{
199 // qDebug() << Q_FUNC_INFO;
200 // cannot add a certificate to the system store
201 if (in.store == KSslCaCertificate::SystemStore) {
202 Q_ASSERT(false);
203 return false;
204 }
205 if (knownCerts.contains(in.certHash)) {
206 Q_ASSERT(false);
207 return false;
208 }
209
210 QString certFilename = userCertDir + QString::fromLatin1(in.certHash);
211
212 QFile certFile(certFilename);
213 if (!QDir().mkpath(userCertDir) || certFile.open(QIODevice::ReadOnly)) {
214 return false;
215 }
216 if (!certFile.open(QIODevice::WriteOnly)) {
217 return false;
218 }
219 if (certFile.write(in.cert.toPem()) < 1) {
220 return false;
221 }
222 knownCerts.insert(in.certHash);
223
224 updateCertificateBlacklisted(in);
225
226 return true;
227}
228
229bool KSslCertificateManagerPrivate::removeCertificate(const KSslCaCertificate &old)
230{
231 // qDebug() << Q_FUNC_INFO;
232 // cannot remove a certificate from the system store
233 if (old.store == KSslCaCertificate::SystemStore) {
234 Q_ASSERT(false);
235 return false;
236 }
237
238 if (!QFile::remove(userCertDir + QString::fromLatin1(old.certHash))) {
239 // suppose somebody copied a certificate file into userCertDir without changing the
240 // filename to the digest.
241 // the rest of the code will work fine because it loads all certificate files from
242 // userCertDir without asking for the name, we just can't remove the certificate using
243 // its digest as filename - so search the whole directory.
244 // if the certificate was added with the digest as name *and* with a different name, we
245 // still fail to remove it completely at first try - BAD USER! BAD!
246
247 bool removed = false;
248 QDir dir(userCertDir);
249 const QStringList dirList = dir.entryList(QDir::Files);
250 for (const QString &certFilename : dirList) {
251 const QString certPath = userCertDir + certFilename;
253
254 if (!certs.isEmpty() && certs.at(0).digest().toHex() == old.certHash) {
255 if (QFile::remove(certPath)) {
256 removed = true;
257 } else {
258 // maybe the file is readable but not writable
259 return false;
260 }
261 }
262 }
263 if (!removed) {
264 // looks like the file is not there
265 return false;
266 }
267 }
268
269 // note that knownCerts *should* need no updating due to the way setAllCertificates() works -
270 // it should never call addCertificate and removeCertificate for the same cert in one run
271
272 // clean up the blacklist
273 setCertificateBlacklisted(old.certHash, false);
274
275 return true;
276}
277
278static bool certLessThan(const KSslCaCertificate &cacert1, const KSslCaCertificate &cacert2)
279{
280 if (cacert1.store != cacert2.store) {
281 // SystemStore is numerically smaller so the system certs come first; this is important
282 // so that system certificates come first in case the user added an already-present
283 // certificate as a user certificate.
284 return cacert1.store < cacert2.store;
285 }
286 return cacert1.certHash < cacert2.certHash;
287}
288
289void KSslCertificateManagerPrivate::setAllCertificates(const QList<KSslCaCertificate> &certsIn)
290{
291 Q_ASSERT(knownCerts.isEmpty());
292 QList<KSslCaCertificate> in = certsIn;
293 QList<KSslCaCertificate> old = allCertificates();
294 std::sort(in.begin(), in.end(), certLessThan);
295 std::sort(old.begin(), old.end(), certLessThan);
296
297 for (int ii = 0, oi = 0; ii < in.size() || oi < old.size(); ++ii, ++oi) {
298 // look at all elements in both lists, even if we reach the end of one early.
299 if (ii >= in.size()) {
300 removeCertificate(old.at(oi));
301 continue;
302 } else if (oi >= old.size()) {
303 addCertificate(in.at(ii));
304 continue;
305 }
306
307 if (certLessThan(old.at(oi), in.at(ii))) {
308 // the certificate in "old" is not in "in". only advance the index of "old".
309 removeCertificate(old.at(oi));
310 ii--;
311 } else if (certLessThan(in.at(ii), old.at(oi))) {
312 // the certificate in "in" is not in "old". only advance the index of "in".
313 addCertificate(in.at(ii));
314 oi--;
315 } else { // in.at(ii) "==" old.at(oi)
316 if (in.at(ii).cert != old.at(oi).cert) {
317 // hash collision, be prudent(?) and don't do anything.
318 } else {
319 knownCerts.insert(old.at(oi).certHash);
320 if (in.at(ii).isBlacklisted != old.at(oi).isBlacklisted) {
321 updateCertificateBlacklisted(in.at(ii));
322 }
323 }
324 }
325 }
326 knownCerts.clear();
327 QMutexLocker certListLocker(&certListMutex);
328 isCertListLoaded = false;
329 loadDefaultCaCertificates();
330}
331
332QList<KSslCaCertificate> KSslCertificateManagerPrivate::allCertificates() const
333{
334 // qDebug() << Q_FUNC_INFO;
336 const QList<QSslCertificate> list = deduplicate(QSslConfiguration::systemCaCertificates());
337 for (const QSslCertificate &cert : list) {
338 ret += KSslCaCertificate(cert, KSslCaCertificate::SystemStore, false);
339 }
340
341 const QList<QSslCertificate> userList = QSslCertificate::fromPath(userCertDir + QLatin1Char('*'), QSsl::Pem, QSslCertificate::PatternSyntax::Wildcard);
342 for (const QSslCertificate &cert : userList) {
343 ret += KSslCaCertificate(cert, KSslCaCertificate::UserStore, false);
344 }
345
346 KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig);
347 KConfigGroup group = config.group(QStringLiteral("Blacklist of CA Certificates"));
348 for (KSslCaCertificate &cert : ret) {
349 if (group.hasKey(cert.certHash.constData())) {
350 cert.isBlacklisted = true;
351 // qDebug() << "is blacklisted";
352 }
353 }
354
355 return ret;
356}
357
358bool KSslCertificateManagerPrivate::updateCertificateBlacklisted(const KSslCaCertificate &cert)
359{
360 return setCertificateBlacklisted(cert.certHash, cert.isBlacklisted);
361}
362
363bool KSslCertificateManagerPrivate::setCertificateBlacklisted(const QByteArray &certHash, bool isBlacklisted)
364{
365 // qDebug() << Q_FUNC_INFO << isBlacklisted;
366 KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig);
367 KConfigGroup group = config.group(QStringLiteral("Blacklist of CA Certificates"));
368 if (isBlacklisted) {
369 // TODO check against certificate list ?
370 group.writeEntry(certHash.constData(), QString());
371 } else {
372 if (!group.hasKey(certHash.constData())) {
373 return false;
374 }
375 group.deleteEntry(certHash.constData());
376 }
377
378 return true;
379}
380
381class KSslCertificateManagerContainer
382{
383public:
384 KSslCertificateManager sslCertificateManager;
385};
386
387Q_GLOBAL_STATIC(KSslCertificateManagerContainer, g_instance)
388
389KSslCertificateManager::KSslCertificateManager()
390 : d(new KSslCertificateManagerPrivate())
391{
392}
393
394KSslCertificateManager::~KSslCertificateManager() = default;
395
396// static
397KSslCertificateManager *KSslCertificateManager::self()
398{
399 return &g_instance()->sslCertificateManager;
400}
401
402void KSslCertificateManager::setRule(const KSslCertificateRule &rule)
403{
404#ifdef WITH_QTDBUS
405 d->iface->setRule(rule);
406#endif
407}
408
409void KSslCertificateManager::clearRule(const KSslCertificateRule &rule)
410{
411#ifdef WITH_QTDBUS
412 d->iface->clearRule(rule);
413#endif
414}
415
416void KSslCertificateManager::clearRule(const QSslCertificate &cert, const QString &hostName)
417{
418#ifdef WITH_QTDBUS
419 d->iface->clearRule(cert, hostName);
420#endif
421}
422
423KSslCertificateRule KSslCertificateManager::rule(const QSslCertificate &cert, const QString &hostName) const
424{
425#ifdef WITH_QTDBUS
426 return d->iface->rule(cert, hostName);
427#else
428 return KSslCertificateRule();
429#endif
430}
431
432QList<QSslCertificate> KSslCertificateManager::caCertificates() const
433{
434 QMutexLocker certLocker(&d->certListMutex);
435 if (!d->isCertListLoaded) {
436 d->loadDefaultCaCertificates();
437 }
438 return d->defaultCaCertificates;
439}
440
442{
444 // errors not handled in KSSLD
445 std::copy_if(errors.begin(), errors.end(), std::back_inserter(ret), [](const QSslError &e) {
446 return e.error() == QSslError::NoPeerCertificate || e.error() == QSslError::PathLengthExceeded || e.error() == QSslError::NoSslSupport;
447 });
448 return ret;
449}
450
451QList<KSslCaCertificate> _allKsslCaCertificates(KSslCertificateManager *cm)
452{
453 return KSslCertificateManagerPrivate::get(cm)->allCertificates();
454}
455
456void _setAllKsslCaCertificates(KSslCertificateManager *cm, const QList<KSslCaCertificate> &certsIn)
457{
458 KSslCertificateManagerPrivate::get(cm)->setAllCertificates(certsIn);
459}
460
461#ifdef WITH_QTDBUS
462#include "moc_kssld_interface.cpp"
463#endif
KConfigGroup group(const QString &group)
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)
static QList< QSslError > nonIgnorableErrors(const QList< QSslError > &errors)
Returns the subset of errors that cannot be ignored, ie.
void setIgnoredErrors(const QList< QSslError > &errors)
Set the ignored errors for this certificate.
bool isErrorIgnored(QSslError::SslError error) const
Returns whether error is ignored for this certificate.
QList< QSslError > filterErrors(const QList< QSslError > &errors) const
Filter out errors that are already ignored.
KIOCORE_EXPORT MkpathJob * mkpath(const QUrl &url, const QUrl &baseUrl=QUrl(), JobFlags flags=DefaultFlags)
Creates a directory, creating parent directories as needed.
KIOCORE_EXPORT QString dir(const QString &fileClass)
Returns the most recently used directory associated with this file-class.
KIOCORE_EXPORT QStringList list(const QString &fileClass)
Returns a list of directories associated with this file-class.
const char * constData() const const
QByteArray & insert(qsizetype i, QByteArrayView data)
QByteArray toHex(char separator) const const
bool remove()
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
iterator end()
bool isEmpty() const const
qsizetype size() const const
QList< QSslCertificate > fromPath(const QString &path, QSsl::EncodingFormat format, PatternSyntax syntax)
QString fromLatin1(QByteArrayView str)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:54:08 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.