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 
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
42 CertificatePEM = <PEM-encoded certificate> #host entries are all lowercase, thus no clashes
43 
44  */
45 
46 // TODO GUI for managing exception rules
47 
48 KSslCertificateRule::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 
56 KSslCertificateRule::KSslCertificateRule(const KSslCertificateRule &other)
57  : d(new KSslCertificateRulePrivate())
58 {
59  *d = *other.d;
60 }
61 
62 KSslCertificateRule::~KSslCertificateRule() = default;
63 
64 KSslCertificateRule &KSslCertificateRule::operator=(const KSslCertificateRule &other)
65 {
66  *d = *other.d;
67  return *this;
68 }
69 
70 QSslCertificate KSslCertificateRule::certificate() const
71 {
72  return d->certificate;
73 }
74 
75 QString KSslCertificateRule::hostName() const
76 {
77  return d->hostName;
78 }
79 
80 void KSslCertificateRule::setExpiryDateTime(const QDateTime &dateTime)
81 {
82  d->expiryDateTime = dateTime;
83 }
84 
85 QDateTime KSslCertificateRule::expiryDateTime() const
86 {
87  return d->expiryDateTime;
88 }
89 
90 void KSslCertificateRule::setRejected(bool rejected)
91 {
92  d->isRejected = rejected;
93 }
94 
95 bool KSslCertificateRule::isRejected() const
96 {
97  return d->isRejected;
98 }
99 
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 
125 QList<QSslError::SslError> KSslCertificateRule::ignoredErrors() const
126 {
127  return d->ignoredErrors;
128 }
129 
131 {
132  QList<QSslError> ret;
133  for (const QSslError &error : errors) {
134  if (!isErrorIgnored(error.error())) {
135  ret.append(error);
136  }
137  }
138  return ret;
139 }
140 
141 ////////////////////////////////////////////////////////////////////
142 
143 static 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 
157 KSslCertificateManagerPrivate::KSslCertificateManagerPrivate()
158  : config(QStringLiteral("ksslcertificatemanager"), KConfig::SimpleConfig)
159  , iface(new org::kde::KSSLDInterface(QStringLiteral("org.kde.kssld5"), QStringLiteral("/modules/kssld"), QDBusConnection::sessionBus()))
160  , isCertListLoaded(false)
161  , userCertDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kssl/userCaCertificates/"))
162 {
163 }
164 
165 KSslCertificateManagerPrivate::~KSslCertificateManagerPrivate()
166 {
167  delete iface;
168  iface = nullptr;
169 }
170 
171 void KSslCertificateManagerPrivate::loadDefaultCaCertificates()
172 {
173  defaultCaCertificates.clear();
174 
176 
177  KConfig config(QStringLiteral("ksslcablacklist"), KConfig::SimpleConfig);
178  KConfigGroup group = config.group("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 
192 bool 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 
224 bool 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 
273 static 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 
284 void 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 
327 QList<KSslCaCertificate> KSslCertificateManagerPrivate::allCertificates() const
328 {
329  // qDebug() << Q_FUNC_INFO;
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("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 
353 bool KSslCertificateManagerPrivate::updateCertificateBlacklisted(const KSslCaCertificate &cert)
354 {
355  return setCertificateBlacklisted(cert.certHash, cert.isBlacklisted);
356 }
357 
358 bool 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("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 
376 class KSslCertificateManagerContainer
377 {
378 public:
379  KSslCertificateManager sslCertificateManager;
380 };
381 
382 Q_GLOBAL_STATIC(KSslCertificateManagerContainer, g_instance)
383 
384 KSslCertificateManager::KSslCertificateManager()
385  : d(new KSslCertificateManagerPrivate())
386 {
387 }
388 
389 KSslCertificateManager::~KSslCertificateManager() = default;
390 
391 // static
392 KSslCertificateManager *KSslCertificateManager::self()
393 {
394  return &g_instance()->sslCertificateManager;
395 }
396 
397 void KSslCertificateManager::setRule(const KSslCertificateRule &rule)
398 {
399  d->iface->setRule(rule);
400 }
401 
402 void KSslCertificateManager::clearRule(const KSslCertificateRule &rule)
403 {
404  d->iface->clearRule(rule);
405 }
406 
407 void KSslCertificateManager::clearRule(const QSslCertificate &cert, const QString &hostName)
408 {
409  d->iface->clearRule(cert, hostName);
410 }
411 
412 KSslCertificateRule KSslCertificateManager::rule(const QSslCertificate &cert, const QString &hostName) const
413 {
414  return d->iface->rule(cert, hostName);
415 }
416 
417 QList<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 {
428  QList<QSslError> ret;
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 
436 QList<KSslCaCertificate> _allKsslCaCertificates(KSslCertificateManager *cm)
437 {
438  return KSslCertificateManagerPrivate::get(cm)->allCertificates();
439 }
440 
441 void _setAllKsslCaCertificates(KSslCertificateManager *cm, const QList<KSslCaCertificate> &certsIn)
442 {
443  KSslCertificateManagerPrivate::get(cm)->setAllCertificates(certsIn);
444 }
445 
446 #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()
static QList< QSslError > nonIgnorableErrors(const QList< QSslError > &errors)
Returns the subset of errors that cannot be ignored, ie.
KIOCORE_EXPORT QStringList list(const QString &fileClass)
Returns a list of directories associated with this file-class.
Definition: krecentdirs.cpp:33
Q_GLOBAL_STATIC(Internal::StaticControl, s_instance) class ControlPrivate
int size() const const
const T & at(int i) const const
KConfigGroup group(const char *group)
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
QByteArray toHex() const const
KIOCORE_EXPORT QString dir(const QString &fileClass)
Returns the most recently used directory associated with this file-class.
Definition: krecentdirs.cpp:41
const char * constData() const const
QString fromLatin1(const char *str, int size)
QByteArray digest(QCryptographicHash::Algorithm algorithm) const const
void setIgnoredErrors(const QList< QSslError > &errors)
Set the ignored errors for this certificate.
QList::iterator begin()
QList< QSslError > filterErrors(const QList< QSslError > &errors) const
Filter out errors that are already ignored.
QList< QSslCertificate > systemCaCertificates()
QList::iterator end()
bool isErrorIgnored(QSslError::SslError error) const
Returns whether error is ignored for this certificate.
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 Wed Jun 7 2023 03:55:58 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.