diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -99,6 +99,8 @@ utils/formatting.h utils/gnupg-registry.c utils/gnupg-registry.h + utils/gnupgrelease.cpp + utils/gnupgrelease.h utils/gnupg.cpp utils/gnupg.h utils/hex.cpp @@ -293,6 +295,7 @@ FileSystemWatcher Formatting GnuPG + GnuPGRelease Hex KeyHelpers QtStlHelpers diff --git a/src/utils/gnupgrelease.h b/src/utils/gnupgrelease.h new file mode 100644 --- /dev/null +++ b/src/utils/gnupgrelease.h @@ -0,0 +1,63 @@ +/* -*- mode: c++; c-basic-offset:4 -*- + utils/gnupgrelease.h + + This file is part of Kleopatra, the KDE keymanager + SPDX-FileCopyrightText: 2023 g10 Code GmbH + SPDX-FileContributor: Andre Heinecke + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kleo_export.h" +#include +#include +#include + +namespace Kleo +{ + +/** Handle specific overrides and integrity checks + * used by Kleopatra distributed as part of a GnuPG binary + * package. The customization is done by evaluating + * a signed VERSION file in the parent directory of the + * Kleopatra executable. */ +class KLEO_EXPORT GnuPGRelease : public QObject +{ + Q_OBJECT + +protected: + explicit GnuPGRelease(); + +public: + static GnuPGRelease *instance(); + ~GnuPGRelease() override; + + /* Start checking the VERSION file */ + void startCheck(); + + /** Returns true if the VERSION was verfied + * against the distribution keys installed by GnuPG. */ + bool isGnuPGRelease() const; + + /** Returns true if the meta information was checked + * even if the verification failed. */ + bool checked() const; + + QString description() const; + QString longDescription() const; + QString customIcon() const; + QString customTitle() const; + QString customStatus() const; + QString version() const; + +Q_SIGNALS: + /** Emitted when the version verification is done. */ + void checkDone(); + +private: + class Private; + QScopedPointer const d; +}; +} // namespace Kleo diff --git a/src/utils/gnupgrelease.cpp b/src/utils/gnupgrelease.cpp new file mode 100644 --- /dev/null +++ b/src/utils/gnupgrelease.cpp @@ -0,0 +1,192 @@ +/* -*- mode: c++; c-basic-offset:4 -*- + utils/gnupgrelease.cpp + + This file is part of Kleopatra, the KDE keymanager + SPDX-FileCopyrightText: 2023 g10 Code GmbH + SPDX-FileContributor: Andre Heinecke + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "gnupgrelease.h" + +#include "gnupg.h" +#include "libkleo_debug.h" + +#include +#include +#include + +#ifdef Q_OS_WIN +#define GPG_NAME "/gpg.exe" +#define GPGV_NAME "/gpgv.exe" +#else +#define GPG_NAME "/gpg" +#define GPGV_NAME "/gpgv" +#endif + +/* Relative path to the keys to use to determine if this version + * is distributed by GnuPG */ +#define DISTSIGKEY_RELPATH "../share/gnupg/distsigkey.gpg" +/* Relative path to the VERSION meta information file */ +#define VERSION_RELPATH "../VERSION" + +using namespace Kleo; + +class GnuPGRelease::Private +{ + friend class GnuPGRelease; + GnuPGRelease *const q; + +public: + explicit Private(GnuPGRelease *qq) + : q(qq) + { + } + + void startCheck() + { + const auto instPath = Kleo::gpg4winInstallPath(); + const auto verPath = instPath + QStringLiteral(VERSION_RELPATH); + auto versionFile = new QFile(verPath); + + if (!versionFile->open(QIODevice::ReadOnly)) { + // This is the normal case for other distributions. + // qCDebug(LIBKLEO_LOG) << "No VERSION file found"; + mChecked = true; + Q_EMIT q->checkDone(); + return; + } + const auto sigPath = verPath + QStringLiteral(".sig"); + QFileInfo versionSig(instPath + QStringLiteral(VERSION_RELPATH ".sig")); + if (!versionSig.exists()) { + mChecked = true; + qCDebug(LIBKLEO_LOG) << "No signed VERSION file found."; + Q_EMIT q->checkDone(); + return; + } + // We have a signed version so let us check it against the GnuPG + // release keys. + auto process = new QProcess; + process->setProgram(Kleo::gpgPath().replace(QStringLiteral(GPG_NAME), QStringLiteral(GPGV_NAME))); + const auto keyringPath(QStringLiteral("%1/" DISTSIGKEY_RELPATH).arg(Kleo::gnupgInstallPath())); + process->setArguments(QStringList() << QStringLiteral("--keyring") << keyringPath << QStringLiteral("--") << sigPath << verPath); + + QObject::connect(process, + qOverload(&QProcess::finished), + [process, versionFile, this](int exitCode, QProcess::ExitStatus exitStatus) { + /* Read the version string regardless of signature status */ + mVersion = readVersLine(versionFile); + if (exitStatus == QProcess::NormalExit && exitCode == 0) { + qCDebug(LIBKLEO_LOG) << "VERSION has valid signature."; + mIsGnuPGRelease = true; + /* Well a more complex format might make sense here at some point. For + * historic reasons this is a very simple line based file.*/ + mDescription = readVersLine(versionFile); + mDescLong = readVersLine(versionFile); + mCustomWindowTitle = readVersLine(versionFile); + mCustomIcon = readVersLine(versionFile); + mCustomStatus = readVersLine(versionFile); + } else { + qCDebug(LIBKLEO_LOG).nospace() << "gpgv (" << process << ") crashed (exit code: " << exitCode << ")"; + qCDebug(LIBKLEO_LOG) << "gpgv failed with stderr: " << process->readAllStandardError(); + qCDebug(LIBKLEO_LOG) << "gpgv stdout" << process->readAllStandardOutput(); + } + const auto backendVersions = backendVersionInfo(); + mDescLong = i18nc("Preceeds a list of applications/libraries used by Kleopatra", + "Uses:") + + QLatin1String{"
  • "} + + backendVersions.join(QLatin1String{"
  • "}) + + QLatin1String{"
"} + + mDescLong); + mChecked = true; + delete versionFile; + Q_EMIT q->checkDone(); + process->deleteLater(); + }); + + qCDebug(LIBKLEO_LOG).nospace() << "Starting gpgv (" << process << ") with arguments " << process->arguments().join(QLatin1Char(' ')) << " ..."; + process->start(); + } + +private: + QString readVersLine(QFile *versionFile) + { + return QString::fromUtf8(versionFile->readLine()).trimmed(); + } + + QString mVersion; + QString mDescription; + QString mDescLong; + QString mCustomWindowTitle; + QString mCustomIcon; + QString mCustomStatus; + bool mIsGnuPGRelease = false; + bool mChecked = false; +}; + +GnuPGRelease::GnuPGRelease() + : QObject() + , d(new Private(this)) +{ +} + +GnuPGRelease::~GnuPGRelease() +{ +} + +/* static */ +GnuPGRelease *GnuPGRelease::instance() +{ + // We use singleton to do the signature check only once. + static GnuPGRelease *inst = nullptr; + if (!inst) { + inst = new GnuPGRelease(); + } + return inst; +} + +void GnuPGRelease::startCheck() +{ + d->startCheck(); +} + +bool GnuPGRelease::isGnuPGRelease() const +{ + return d->mIsGnuPGRelease; +} + +bool GnuPGRelease::checked() const +{ + return d->mChecked; +} + +QString GnuPGRelease::version() const +{ + return d->mVersion; +} + +QString GnuPGRelease::description() const +{ + return d->mDescription; +} + +QString GnuPGRelease::longDescription() const +{ + return d->mDescLong; +} + +QString GnuPGRelease::customTitle() const +{ + return d->mCustomWindowTitle; +} + +QString GnuPGRelease::customIcon() const +{ + return d->mCustomIcon; +} + +QString GnuPGRelease::customStatus() const +{ + return d->mCustomStatus; +}