KIO

ksslcertificatemanager.cpp
1 /*
2  This file is part of the KDE project
3  SPDX-FileCopyrightText: 2007, 2008, 2010 Andreas Hartmetz <[email protected]>
4 
5  SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "ksslcertificatemanager.h"
9 #include "ksslcertificatemanager_p.h"
10 #include "ksslerror_p.h"
11 
12 #include "kssld_interface.h"
13 #include "ksslerroruidata_p.h"
14 
15 #include <KConfig>
16 #include <KConfigGroup>
17 #include <KLocalizedString>
18 
19 #include <QDBusConnection>
20 #include <QDBusConnectionInterface>
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
43 CertificatePEM = <PEM-encoded certificate> #host entries are all lowercase, thus no clashes
44 
45  */
46 
47 // TODO GUI for managing exception rules
48 
49 KSslCertificateRule::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 
57 KSslCertificateRule::KSslCertificateRule(const KSslCertificateRule &other)
58  : d(new KSslCertificateRulePrivate())
59 {
60  *d = *other.d;
61 }
62 
63 KSslCertificateRule::~KSslCertificateRule() = default;
64 
65 KSslCertificateRule &KSslCertificateRule::operator=(const KSslCertificateRule &other)
66 {
67  *d = *other.d;
68  return *this;
69 }
70 
71 QSslCertificate KSslCertificateRule::certificate() const
72 {
73  return d->certificate;
74 }
75 
76 QString KSslCertificateRule::hostName() const
77 {
78  return d->hostName;
79 }
80 
81 void KSslCertificateRule::setExpiryDateTime(const QDateTime &dateTime)
82 {
83  d->expiryDateTime = dateTime;
84 }
85 
86 QDateTime KSslCertificateRule::expiryDateTime() const
87 {
88  return d->expiryDateTime;
89 }
90 
91 void KSslCertificateRule::setRejected(bool rejected)
92 {
93  d->isRejected = rejected;
94 }
95 
96 bool KSslCertificateRule::isRejected() const
97 {
98  return d->isRejected;
99 }
100 
101 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 64)
102 bool KSslCertificateRule::isErrorIgnored(KSslError::Error error) const
103 {
104  return d->ignoredErrors.contains(KSslErrorPrivate::errorFromKSslError(error));
105 }
106 #endif
107 
109 {
110  return d->ignoredErrors.contains(error);
111 }
112 
113 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 64)
115 {
116  d->ignoredErrors.clear();
117  // ### Quadratic runtime, woohoo! Use a QSet if that should ever be an issue.
118  for (KSslError::Error e : errors) {
119  QSslError::SslError error = KSslErrorPrivate::errorFromKSslError(e);
120  if (!isErrorIgnored(error)) {
121  d->ignoredErrors.append(error);
122  }
123  }
124 }
125 #endif
126 
127 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 64)
129 {
131  el.reserve(errors.size());
132  for (const KSslError &e : errors) {
133  el.append(e.error());
134  }
135  setIgnoredErrors(el);
136 }
137 #endif
138 
140 {
141  d->ignoredErrors.clear();
142  for (const QSslError &error : errors) {
143  if (!isErrorIgnored(error.error())) {
144  d->ignoredErrors.append(error.error());
145  }
146  }
147 }
148 
150 {
151  d->ignoredErrors.clear();
152  for (QSslError::SslError error : errors) {
153  if (!isErrorIgnored(error)) {
154  d->ignoredErrors.append(error);
155  }
156  }
157 }
158 
159 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 64)
160 QList<KSslError::Error> KSslCertificateRule::ignoredErrors() const
161 {
162  // KF6: replace by QList<QSslError::SslError> below
164  errors.reserve(d->ignoredErrors.size());
165  std::transform(d->ignoredErrors.cbegin(), d->ignoredErrors.cend(), std::back_inserter(errors), KSslErrorPrivate::errorFromQSslError);
166  return errors;
167 }
168 #else
169 QList<QSslError::SslError> KSslCertificateRule::ignoredErrors() const
170 {
171  return d->ignoredErrors;
172 }
173 #endif
174 
175 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 64)
177 {
179  for (KSslError::Error error : errors) {
180  if (!isErrorIgnored(error)) {
181  ret.append(error);
182  }
183  }
184  return ret;
185 }
186 #endif
187 
188 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 64)
190 {
191  QList<KSslError> ret;
192  for (const KSslError &error : errors) {
193  if (!isErrorIgnored(error.error())) {
194  ret.append(error);
195  }
196  }
197  return ret;
198 }
199 #endif
200 
202 {
203  QList<QSslError> ret;
204  for (const QSslError &error : errors) {
205  if (!isErrorIgnored(error.error())) {
206  ret.append(error);
207  }
208  }
209  return ret;
210 }
211 
212 ////////////////////////////////////////////////////////////////////
213 
214 static QList<QSslCertificate> deduplicate(const QList<QSslCertificate> &certs)
215 {
216  std::set<QByteArray> digests;
218  for (const QSslCertificate &cert : certs) {
219  QByteArray digest = cert.digest();
220  const auto [it, isInserted] = digests.insert(digest);
221  if (isInserted) {
222  ret.append(cert);
223  }
224  }
225  return ret;
226 }
227 
228 KSslCertificateManagerPrivate::KSslCertificateManagerPrivate()
229  : config(QStringLiteral("ksslcertificatemanager"), KConfig::SimpleConfig)
230  , iface(new org::kde::KSSLDInterface(QStringLiteral("org.kde.kssld5"), QStringLiteral("/modules/kssld"), QDBusConnection::sessionBus()))
231  , isCertListLoaded(false)
232  , userCertDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kssl/userCaCertificates/"))
233 {
234 }
235 
236 KSslCertificateManagerPrivate::~KSslCertificateManagerPrivate()
237 {
238  delete iface;
239  iface = nullptr;
240 }
241 
242 void KSslCertificateManagerPrivate::loadDefaultCaCertificates()
243 {
244  defaultCaCertificates.clear();
245 
247 
248  KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig);
249  KConfigGroup group = config.group("Blacklist of CA Certificates");
250 
251  certs.append(QSslCertificate::fromPath(userCertDir + QLatin1Char('*'), QSsl::Pem, QSslCertificate::PatternSyntax::Wildcard));
252 
253  for (const QSslCertificate &cert : std::as_const(certs)) {
254  const QByteArray digest = cert.digest().toHex();
255  if (!group.hasKey(digest.constData())) {
256  defaultCaCertificates += cert;
257  }
258  }
259 
260  isCertListLoaded = true;
261 }
262 
263 bool KSslCertificateManagerPrivate::addCertificate(const KSslCaCertificate &in)
264 {
265  // qDebug() << Q_FUNC_INFO;
266  // cannot add a certificate to the system store
267  if (in.store == KSslCaCertificate::SystemStore) {
268  Q_ASSERT(false);
269  return false;
270  }
271  if (knownCerts.contains(in.certHash)) {
272  Q_ASSERT(false);
273  return false;
274  }
275 
276  QString certFilename = userCertDir + QString::fromLatin1(in.certHash);
277 
278  QFile certFile(certFilename);
279  if (!QDir().mkpath(userCertDir) || certFile.open(QIODevice::ReadOnly)) {
280  return false;
281  }
282  if (!certFile.open(QIODevice::WriteOnly)) {
283  return false;
284  }
285  if (certFile.write(in.cert.toPem()) < 1) {
286  return false;
287  }
288  knownCerts.insert(in.certHash);
289 
290  updateCertificateBlacklisted(in);
291 
292  return true;
293 }
294 
295 bool KSslCertificateManagerPrivate::removeCertificate(const KSslCaCertificate &old)
296 {
297  // qDebug() << Q_FUNC_INFO;
298  // cannot remove a certificate from the system store
299  if (old.store == KSslCaCertificate::SystemStore) {
300  Q_ASSERT(false);
301  return false;
302  }
303 
304  if (!QFile::remove(userCertDir + QString::fromLatin1(old.certHash))) {
305  // suppose somebody copied a certificate file into userCertDir without changing the
306  // filename to the digest.
307  // the rest of the code will work fine because it loads all certificate files from
308  // userCertDir without asking for the name, we just can't remove the certificate using
309  // its digest as filename - so search the whole directory.
310  // if the certificate was added with the digest as name *and* with a different name, we
311  // still fail to remove it completely at first try - BAD USER! BAD!
312 
313  bool removed = false;
314  QDir dir(userCertDir);
315  const QStringList dirList = dir.entryList(QDir::Files);
316  for (const QString &certFilename : dirList) {
317  const QString certPath = userCertDir + certFilename;
319 
320  if (!certs.isEmpty() && certs.at(0).digest().toHex() == old.certHash) {
321  if (QFile::remove(certPath)) {
322  removed = true;
323  } else {
324  // maybe the file is readable but not writable
325  return false;
326  }
327  }
328  }
329  if (!removed) {
330  // looks like the file is not there
331  return false;
332  }
333  }
334 
335  // note that knownCerts *should* need no updating due to the way setAllCertificates() works -
336  // it should never call addCertificate and removeCertificate for the same cert in one run
337 
338  // clean up the blacklist
339  setCertificateBlacklisted(old.certHash, false);
340 
341  return true;
342 }
343 
344 static bool certLessThan(const KSslCaCertificate &cacert1, const KSslCaCertificate &cacert2)
345 {
346  if (cacert1.store != cacert2.store) {
347  // SystemStore is numerically smaller so the system certs come first; this is important
348  // so that system certificates come first in case the user added an already-present
349  // certificate as a user certificate.
350  return cacert1.store < cacert2.store;
351  }
352  return cacert1.certHash < cacert2.certHash;
353 }
354 
355 void KSslCertificateManagerPrivate::setAllCertificates(const QList<KSslCaCertificate> &certsIn)
356 {
357  Q_ASSERT(knownCerts.isEmpty());
358  QList<KSslCaCertificate> in = certsIn;
359  QList<KSslCaCertificate> old = allCertificates();
360  std::sort(in.begin(), in.end(), certLessThan);
361  std::sort(old.begin(), old.end(), certLessThan);
362 
363  for (int ii = 0, oi = 0; ii < in.size() || oi < old.size(); ++ii, ++oi) {
364  // look at all elements in both lists, even if we reach the end of one early.
365  if (ii >= in.size()) {
366  removeCertificate(old.at(oi));
367  continue;
368  } else if (oi >= old.size()) {
369  addCertificate(in.at(ii));
370  continue;
371  }
372 
373  if (certLessThan(old.at(oi), in.at(ii))) {
374  // the certificate in "old" is not in "in". only advance the index of "old".
375  removeCertificate(old.at(oi));
376  ii--;
377  } else if (certLessThan(in.at(ii), old.at(oi))) {
378  // the certificate in "in" is not in "old". only advance the index of "in".
379  addCertificate(in.at(ii));
380  oi--;
381  } else { // in.at(ii) "==" old.at(oi)
382  if (in.at(ii).cert != old.at(oi).cert) {
383  // hash collision, be prudent(?) and don't do anything.
384  } else {
385  knownCerts.insert(old.at(oi).certHash);
386  if (in.at(ii).isBlacklisted != old.at(oi).isBlacklisted) {
387  updateCertificateBlacklisted(in.at(ii));
388  }
389  }
390  }
391  }
392  knownCerts.clear();
393  QMutexLocker certListLocker(&certListMutex);
394  isCertListLoaded = false;
395  loadDefaultCaCertificates();
396 }
397 
398 QList<KSslCaCertificate> KSslCertificateManagerPrivate::allCertificates() const
399 {
400  // qDebug() << Q_FUNC_INFO;
403  for (const QSslCertificate &cert : list) {
404  ret += KSslCaCertificate(cert, KSslCaCertificate::SystemStore, false);
405  }
406 
407  const QList<QSslCertificate> userList = QSslCertificate::fromPath(userCertDir + QLatin1Char('*'), QSsl::Pem, QSslCertificate::PatternSyntax::Wildcard);
408  for (const QSslCertificate &cert : userList) {
409  ret += KSslCaCertificate(cert, KSslCaCertificate::UserStore, false);
410  }
411 
412  KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig);
413  KConfigGroup group = config.group("Blacklist of CA Certificates");
414  for (KSslCaCertificate &cert : ret) {
415  if (group.hasKey(cert.certHash.constData())) {
416  cert.isBlacklisted = true;
417  // qDebug() << "is blacklisted";
418  }
419  }
420 
421  return ret;
422 }
423 
424 bool KSslCertificateManagerPrivate::updateCertificateBlacklisted(const KSslCaCertificate &cert)
425 {
426  return setCertificateBlacklisted(cert.certHash, cert.isBlacklisted);
427 }
428 
429 bool KSslCertificateManagerPrivate::setCertificateBlacklisted(const QByteArray &certHash, bool isBlacklisted)
430 {
431  // qDebug() << Q_FUNC_INFO << isBlacklisted;
432  KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig);
433  KConfigGroup group = config.group("Blacklist of CA Certificates");
434  if (isBlacklisted) {
435  // TODO check against certificate list ?
436  group.writeEntry(certHash.constData(), QString());
437  } else {
438  if (!group.hasKey(certHash.constData())) {
439  return false;
440  }
441  group.deleteEntry(certHash.constData());
442  }
443 
444  return true;
445 }
446 
447 class KSslCertificateManagerContainer
448 {
449 public:
450  KSslCertificateManager sslCertificateManager;
451 };
452 
453 Q_GLOBAL_STATIC(KSslCertificateManagerContainer, g_instance)
454 
455 KSslCertificateManager::KSslCertificateManager()
456  : d(new KSslCertificateManagerPrivate())
457 {
458 }
459 
460 KSslCertificateManager::~KSslCertificateManager() = default;
461 
462 // static
463 KSslCertificateManager *KSslCertificateManager::self()
464 {
465  return &g_instance()->sslCertificateManager;
466 }
467 
468 void KSslCertificateManager::setRule(const KSslCertificateRule &rule)
469 {
470  d->iface->setRule(rule);
471 }
472 
473 void KSslCertificateManager::clearRule(const KSslCertificateRule &rule)
474 {
475  d->iface->clearRule(rule);
476 }
477 
478 void KSslCertificateManager::clearRule(const QSslCertificate &cert, const QString &hostName)
479 {
480  d->iface->clearRule(cert, hostName);
481 }
482 
483 KSslCertificateRule KSslCertificateManager::rule(const QSslCertificate &cert, const QString &hostName) const
484 {
485  return d->iface->rule(cert, hostName);
486 }
487 
488 QList<QSslCertificate> KSslCertificateManager::caCertificates() const
489 {
490  QMutexLocker certLocker(&d->certListMutex);
491  if (!d->isCertListLoaded) {
492  d->loadDefaultCaCertificates();
493  }
494  return d->defaultCaCertificates;
495 }
496 
497 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 64)
498 // static
500 {
501  QList<KSslError> ret;
502  // errors not handled in KSSLD
503  std::copy_if(errors.begin(), errors.end(), std::back_inserter(ret), [](const KSslError &e) {
504  return e.error() == KSslError::NoPeerCertificate || e.error() == KSslError::PathLengthExceeded;
505  });
506  return ret;
507 }
508 #endif
509 
510 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 64)
511 // static
513 {
515  // errors not handled in KSSLD
516  std::copy_if(errors.begin(), errors.end(), std::back_inserter(ret), [](const KSslError::Error &e) {
517  return e == KSslError::NoPeerCertificate || e == KSslError::PathLengthExceeded;
518  });
519  return ret;
520 }
521 #endif
522 
524 {
525  QList<QSslError> ret;
526  // errors not handled in KSSLD
527  std::copy_if(errors.begin(), errors.end(), std::back_inserter(ret), [](const QSslError &e) {
528  return e.error() == QSslError::NoPeerCertificate || e.error() == QSslError::PathLengthExceeded || e.error() == QSslError::NoSslSupport;
529  });
530  return ret;
531 }
532 
533 QList<KSslCaCertificate> _allKsslCaCertificates(KSslCertificateManager *cm)
534 {
535  return KSslCertificateManagerPrivate::get(cm)->allCertificates();
536 }
537 
538 void _setAllKsslCaCertificates(KSslCertificateManager *cm, const QList<KSslCaCertificate> &certsIn)
539 {
540  KSslCertificateManagerPrivate::get(cm)->setAllCertificates(certsIn);
541 }
542 
543 #include "moc_kssld_interface.cpp"
void append(const T &value)
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
void deleteEntry(const char *key, WriteConfigFlags pFlags=Normal)
bool remove()
KIOFILEWIDGETS_EXPORT QStringList list(const QString &fileClass)
Returns a list of directories associated with this file-class.
Definition: krecentdirs.cpp:39
Q_GLOBAL_STATIC(Internal::StaticControl, s_instance) class ControlPrivate
void reserve(int alloc)
void setIgnoredErrors(const QList< KSslError::Error > &errors)
int size() const const
bool isErrorIgnored(KSslError::Error error) const
const T & at(int i) const const
QList< KSslError::Error > filterErrors(const QList< KSslError::Error > &errors) const
bool isEmpty() const const
bool hasKey(const char *key) const
KIOCORE_EXPORT MkpathJob * mkpath(const QUrl &url, const QUrl &baseUrl=QUrl(), JobFlags flags=DefaultFlags)
Creates a directory, creating parent directories as needed.
Definition: mkpathjob.cpp:148
KSharedConfigPtr config()
QByteArray toHex() const const
To be replaced by QSslError.
Definition: ktcpsocket.h:103
KIOFILEWIDGETS_EXPORT QString dir(const QString &fileClass)
Returns the most recently used directory associated with this file-class.
Definition: krecentdirs.cpp:47
static QList< KSslError > nonIgnorableErrors(const QList< KSslError > &errors)
const char * constData() const const
QString fromLatin1(const char *str, int size)
QByteArray digest(QCryptographicHash::Algorithm algorithm) const const
QList::iterator begin()
QList< QSslCertificate > systemCaCertificates()
QList::iterator end()
QList< QSslCertificate > fromPath(const QString &path, QSsl::EncodingFormat format, QRegExp::PatternSyntax syntax)
virtual QVariant get(ScriptableExtension *callerPrincipal, quint64 objId, const QString &propName)
QByteArray & insert(int i, char ch)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Tue Feb 7 2023 04:00:36 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.