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

KDE's Doxygen guidelines are available online.