File indexing completed on 2024-06-23 05:13:38
0001 /* -*- mode: c++; c-basic-offset:4 -*- 0002 commands/changeroottrustcommand.cpp 0003 0004 This file is part of Kleopatra, the KDE keymanager 0005 SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB 0006 0007 SPDX-License-Identifier: GPL-2.0-or-later 0008 */ 0009 0010 #include <config-kleopatra.h> 0011 0012 #include "changeroottrustcommand.h" 0013 #include "command_p.h" 0014 0015 #include <Libkleo/Dn> 0016 #include <Libkleo/GnuPG> 0017 #include <Libkleo/KeyCache> 0018 0019 #include "kleopatra_debug.h" 0020 #include <KLocalizedString> 0021 #include <QSaveFile> 0022 0023 #include <QByteArray> 0024 #include <QDir> 0025 #include <QFile> 0026 #include <QMutex> 0027 #include <QMutexLocker> 0028 #include <QProcess> 0029 #include <QString> 0030 #include <QStringList> 0031 #include <QThread> 0032 0033 #include <gpgme++/key.h> 0034 0035 using namespace Kleo; 0036 using namespace Kleo::Commands; 0037 using namespace GpgME; 0038 0039 class ChangeRootTrustCommand::Private : public QThread, public Command::Private 0040 { 0041 Q_OBJECT 0042 private: 0043 friend class ::Kleo::Commands::ChangeRootTrustCommand; 0044 ChangeRootTrustCommand *q_func() const 0045 { 0046 return static_cast<ChangeRootTrustCommand *>(q); 0047 } 0048 0049 public: 0050 explicit Private(ChangeRootTrustCommand *qq, KeyListController *c) 0051 : QThread() 0052 , Command::Private(qq, c) 0053 , mutex() 0054 , trust(Key::Ultimate) 0055 , trustListFile(QDir(gnupgHomeDirectory()).absoluteFilePath(QStringLiteral("trustlist.txt"))) 0056 , canceled(false) 0057 { 0058 } 0059 0060 private: 0061 void init() 0062 { 0063 q->setWarnWhenRunningAtShutdown(false); 0064 connect(this, &QThread::finished, this, &ChangeRootTrustCommand::Private::slotOperationFinished); 0065 } 0066 0067 void run() override; 0068 0069 private: 0070 void slotOperationFinished() 0071 { 0072 KeyCache::mutableInstance()->enableFileSystemWatcher(true); 0073 if (error.isEmpty()) { 0074 KeyCache::mutableInstance()->reload(GpgME::CMS); 0075 } else 0076 Command::Private::error(i18n("Failed to update the trust database:\n" 0077 "%1", 0078 error), 0079 i18n("Root Trust Update Failed")); 0080 Command::Private::finished(); 0081 } 0082 0083 private: 0084 mutable QMutex mutex; 0085 Key::OwnerTrust trust; 0086 QString trustListFile; 0087 QString gpgConfPath; 0088 QString error; 0089 volatile bool canceled; 0090 }; 0091 0092 ChangeRootTrustCommand::Private *ChangeRootTrustCommand::d_func() 0093 { 0094 return static_cast<Private *>(d.get()); 0095 } 0096 const ChangeRootTrustCommand::Private *ChangeRootTrustCommand::d_func() const 0097 { 0098 return static_cast<const Private *>(d.get()); 0099 } 0100 0101 #define q q_func() 0102 #define d d_func() 0103 0104 ChangeRootTrustCommand::ChangeRootTrustCommand(KeyListController *p) 0105 : Command(new Private(this, p)) 0106 { 0107 d->init(); 0108 } 0109 0110 ChangeRootTrustCommand::ChangeRootTrustCommand(QAbstractItemView *v, KeyListController *p) 0111 : Command(v, new Private(this, p)) 0112 { 0113 d->init(); 0114 } 0115 0116 ChangeRootTrustCommand::ChangeRootTrustCommand(const GpgME::Key &key, KeyListController *p) 0117 : Command(new Private(this, p)) 0118 { 0119 Q_ASSERT(!key.isNull()); 0120 d->init(); 0121 setKey(key); 0122 } 0123 0124 ChangeRootTrustCommand::ChangeRootTrustCommand(const GpgME::Key &key, QAbstractItemView *v, KeyListController *p) 0125 : Command(v, new Private(this, p)) 0126 { 0127 Q_ASSERT(!key.isNull()); 0128 d->init(); 0129 setKey(key); 0130 } 0131 0132 ChangeRootTrustCommand::~ChangeRootTrustCommand() 0133 { 0134 } 0135 0136 void ChangeRootTrustCommand::setTrust(Key::OwnerTrust trust) 0137 { 0138 Q_ASSERT(!d->isRunning()); 0139 const QMutexLocker locker(&d->mutex); 0140 d->trust = trust; 0141 } 0142 0143 Key::OwnerTrust ChangeRootTrustCommand::trust() const 0144 { 0145 const QMutexLocker locker(&d->mutex); 0146 return d->trust; 0147 } 0148 0149 void ChangeRootTrustCommand::setTrustListFile(const QString &trustListFile) 0150 { 0151 Q_ASSERT(!d->isRunning()); 0152 const QMutexLocker locker(&d->mutex); 0153 d->trustListFile = trustListFile; 0154 } 0155 0156 QString ChangeRootTrustCommand::trustListFile() const 0157 { 0158 const QMutexLocker locker(&d->mutex); 0159 return d->trustListFile; 0160 } 0161 0162 void ChangeRootTrustCommand::doStart() 0163 { 0164 const std::vector<Key> keys = d->keys(); 0165 Key key; 0166 if (keys.size() == 1) { 0167 key = keys.front(); 0168 } else { 0169 qCWarning(KLEOPATRA_LOG) << "can only work with one certificate at a time"; 0170 } 0171 0172 if (key.isNull()) { 0173 d->Command::Private::finished(); 0174 return; 0175 } 0176 0177 d->gpgConfPath = gpgConfPath(); 0178 KeyCache::mutableInstance()->enableFileSystemWatcher(false); 0179 d->start(); 0180 } 0181 0182 void ChangeRootTrustCommand::doCancel() 0183 { 0184 const QMutexLocker locker(&d->mutex); 0185 d->canceled = true; 0186 } 0187 0188 static QString change_trust_file(const QString &trustListFile, const QString &fingerprint, const DN &dn, Key::OwnerTrust trust); 0189 static QString run_gpgconf_reload_gpg_agent(const QString &gpgConfPath); 0190 0191 void ChangeRootTrustCommand::Private::run() 0192 { 0193 QMutexLocker locker(&mutex); 0194 0195 const auto key = keys().front(); 0196 const QString fpr = QString::fromLatin1(key.primaryFingerprint()); 0197 const auto dn = DN(key.userID(0).id()); 0198 const Key::OwnerTrust trust = this->trust; 0199 const QString trustListFile = this->trustListFile; 0200 const QString gpgConfPath = this->gpgConfPath; 0201 0202 locker.unlock(); 0203 0204 QString err = change_trust_file(trustListFile, fpr, dn, trust); 0205 if (err.isEmpty()) { 0206 err = run_gpgconf_reload_gpg_agent(gpgConfPath); 0207 } 0208 0209 locker.relock(); 0210 0211 this->error = err; 0212 } 0213 0214 static QString add_colons(const QString &fpr) 0215 { 0216 QString result; 0217 result.reserve(fpr.size() / 2 * 3 + 1); 0218 bool needColon = false; 0219 for (QChar ch : fpr) { 0220 result += ch; 0221 if (needColon) { 0222 result += QLatin1Char(':'); 0223 } 0224 needColon = !needColon; 0225 } 0226 if (result.endsWith(QLatin1Char(':'))) { 0227 result.chop(1); 0228 } 0229 return result; 0230 } 0231 0232 namespace 0233 { 0234 0235 // fix stupid default-finalize behaviour... 0236 class KFixedSaveFile : public QSaveFile 0237 { 0238 public: 0239 explicit KFixedSaveFile(const QString &fileName) 0240 : QSaveFile(fileName) 0241 { 0242 } 0243 ~KFixedSaveFile() override 0244 { 0245 cancelWriting(); 0246 } 0247 }; 0248 0249 } 0250 0251 // static 0252 QString change_trust_file(const QString &trustListFile, const QString &key, const DN &dn, Key::OwnerTrust trust) 0253 { 0254 QList<QByteArray> trustListFileContents; 0255 0256 if (QFile::exists(trustListFile)) { // non-existence is not fatal... 0257 if (QFile in(trustListFile); in.open(QIODevice::ReadOnly)) { 0258 trustListFileContents = in.readAll().split('\n'); 0259 // remove last empty line to avoid adding more empty lines when we write the lines 0260 if (!trustListFileContents.empty() && trustListFileContents.back().isEmpty()) { 0261 trustListFileContents.pop_back(); 0262 } 0263 } else { // ...but failure to open an existing file _is_ 0264 return i18n("Cannot open existing file \"%1\" for reading: %2", trustListFile, in.errorString()); 0265 } 0266 // the file is now closed, so KSaveFile doesn't clobber the original 0267 } else { 0268 // the default contents of the trustlist.txt file (see the headerblurb variable in trustlist.c of gnupg); 0269 // we add an additional comment about the "include-default" statement 0270 trustListFileContents = { 0271 "# This is the list of trusted keys. Comment lines, like this one, as", 0272 "# well as empty lines are ignored. Lines have a length limit but this", 0273 "# is not a serious limitation as the format of the entries is fixed and", 0274 "# checked by gpg-agent. A non-comment line starts with optional white", 0275 "# space, followed by the SHA-1 fingerpint in hex, followed by a flag", 0276 "# which may be one of 'P', 'S' or '*' and optionally followed by a list of", 0277 "# other flags. The fingerprint may be prefixed with a '!' to mark the", 0278 "# key as not trusted. You should give the gpg-agent a HUP or run the", 0279 "# command \"gpgconf --reload gpg-agent\" after changing this file.", 0280 "# Additionally to this file, gpg-agent will read the default trust list file", 0281 "# if the statement \"include-default\" is used below.", 0282 "", 0283 "", 0284 "# Include the default trust list", 0285 "include-default", 0286 "", 0287 }; 0288 } 0289 0290 KFixedSaveFile out(trustListFile); 0291 if (!out.open(QIODevice::WriteOnly)) 0292 return i18n("Cannot open file \"%1\" for reading and writing: %2", out.fileName() /*sic!*/, out.errorString()); 0293 0294 if (!out.setPermissions(QFile::ReadOwner | QFile::WriteOwner)) 0295 return i18n("Cannot set restrictive permissions on file %1: %2", out.fileName() /*sic!*/, out.errorString()); 0296 0297 const QString keyColon = add_colons(key); 0298 0299 qCDebug(KLEOPATRA_LOG) << qPrintable(key) << " -> " << qPrintable(keyColon); 0300 0301 // ( 1) ( 2 ) ( 3 )( 4) 0302 static const char16_t pattern[] = uR"(\s*(!?)\s*([a-fA-F0-9]{40}|(?:[a-fA-F0-9]{2}:){19}[a-fA-F0-9]{2})\s*([SsPp*])(.*))"; 0303 static const QRegularExpression rx(QRegularExpression::anchoredPattern(pattern)); 0304 bool found = false; 0305 0306 for (const QByteArray &rawLine : std::as_const(trustListFileContents)) { 0307 const QString line = QString::fromLatin1(rawLine.data(), rawLine.size()); 0308 const QRegularExpressionMatch match = rx.match(line); 0309 if (!match.hasMatch()) { 0310 qCDebug(KLEOPATRA_LOG) << "line \"" << rawLine.data() << "\" does not match"; 0311 out.write(rawLine + '\n'); 0312 continue; 0313 } 0314 const QString cap2 = match.captured(2); 0315 if (cap2 != key && cap2 != keyColon) { 0316 qCDebug(KLEOPATRA_LOG) << qPrintable(key) << " != " << qPrintable(cap2) << " != " << qPrintable(keyColon); 0317 out.write(rawLine + '\n'); 0318 continue; 0319 } 0320 found = true; 0321 const bool disabled = match.capturedView(1) == QLatin1Char('!'); 0322 const QByteArray flags = match.captured(3).toLatin1(); 0323 const QByteArray rests = match.captured(4).toLatin1(); 0324 if (trust == Key::Ultimate) 0325 if (!disabled) { // unchanged 0326 out.write(rawLine + '\n'); 0327 } else { 0328 out.write(keyColon.toLatin1() + ' ' + flags + rests + '\n'); 0329 } 0330 else if (trust == Key::Never) { 0331 if (disabled) { // unchanged 0332 out.write(rawLine + '\n'); 0333 } else { 0334 out.write('!' + keyColon.toLatin1() + ' ' + flags + rests + '\n'); 0335 } 0336 } 0337 // else: trust == Key::Unknown 0338 // -> don't write - ie.erase 0339 } 0340 0341 if (!found) { // add 0342 out.write("\n"); 0343 // write comment lines with DN attributes 0344 std::for_each(dn.begin(), dn.end(), [&out](const auto &attr) { 0345 out.write("# " + attr.name().toUtf8() + "=" + attr.value().toUtf8() + '\n'); 0346 }); 0347 if (trust == Key::Ultimate) { 0348 out.write(keyColon.toLatin1() + " S relax\n"); 0349 } else if (trust == Key::Never) { 0350 out.write('!' + keyColon.toLatin1() + " S relax\n"); 0351 } 0352 } 0353 0354 if (!out.commit()) 0355 return i18n("Failed to move file %1 to its final destination, %2: %3", out.fileName(), trustListFile, out.errorString()); 0356 0357 return QString(); 0358 } 0359 0360 // static 0361 QString run_gpgconf_reload_gpg_agent(const QString &gpgConfPath) 0362 { 0363 if (gpgConfPath.isEmpty()) { 0364 return i18n("Could not find gpgconf executable"); 0365 } 0366 0367 QProcess p; 0368 p.start(gpgConfPath, QStringList() << QStringLiteral("--reload") << QStringLiteral("gpg-agent")); 0369 qCDebug(KLEOPATRA_LOG) << "starting " << qPrintable(gpgConfPath) << " --reload gpg-agent"; 0370 p.waitForFinished(-1); 0371 qCDebug(KLEOPATRA_LOG) << "done"; 0372 if (p.error() == QProcess::UnknownError) { 0373 return QString(); 0374 } else { 0375 return i18n("\"gpgconf --reload gpg-agent\" failed: %1", p.errorString()); 0376 } 0377 } 0378 0379 #undef q_func 0380 #undef d_func 0381 0382 #include "changeroottrustcommand.moc" 0383 #include "moc_changeroottrustcommand.cpp"