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"