From e77a8ac0cc2ef356ef7e9da2793329432caf7406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20Kl=C3=B6cker?= Date: Fri, 22 Dec 2023 10:05:16 +0100 Subject: [PATCH] qt: Support verification of opaque signed data directly to/from files * lang/qt/src/Makefile.am: Add new files. * lang/qt/src/job.cpp (VerifyOpaqueJob): Move definition of constructor and destructor and inclusion of the moc file to the corresponding .cpp file. * lang/qt/src/verifyopaquejob.cpp: New. * lang/qt/src/verifyopaquejob.h (VerifyOpaqueJob): Add member functions setInputFile, inputFile, setOutputFile, outputFile. * lang/qt/src/verifyopaquejob_p.h: New. * lang/qt/src/qgpgmeverifyopaquejob.cpp (class QGpgMEVerifyOpaqueJobPrivate): New. (QGpgMEVerifyOpaqueJob::QGpgMEVerifyOpaqueJob): Instantiate private job class. (verify_from_filename): New. * lang/qt/tests/Makefile.am: Add new test program. * lang/qt/tests/run-verifyopaquejob.cpp: New. -- This makes it possible to tell gpg to read the input and write the output directly to a specified file bypassing GpgME's Data IO when verifying an opaque signed (or clear signed) file. GnuPG-bug-id: 6550 --- lang/qt/src/Makefile.am | 2 + lang/qt/src/job.cpp | 3 - lang/qt/src/qgpgmeverifyopaquejob.cpp | 88 +++++++++++++- lang/qt/src/verifyopaquejob.cpp | 74 ++++++++++++ lang/qt/src/verifyopaquejob.h | 30 ++++- lang/qt/src/verifyopaquejob_p.h | 50 ++++++++ lang/qt/tests/Makefile.am | 2 + lang/qt/tests/run-verifyopaquejob.cpp | 160 ++++++++++++++++++++++++++ 8 files changed, 399 insertions(+), 10 deletions(-) create mode 100644 lang/qt/src/verifyopaquejob.cpp create mode 100644 lang/qt/src/verifyopaquejob_p.h create mode 100644 lang/qt/tests/run-verifyopaquejob.cpp diff --git a/lang/qt/src/Makefile.am b/lang/qt/src/Makefile.am index ef625d89..81e07ce6 100644 --- a/lang/qt/src/Makefile.am +++ b/lang/qt/src/Makefile.am @@ -69,6 +69,7 @@ qgpgme_sources = \ signjob.cpp \ dn.cpp cryptoconfig.cpp wkdlookupresult.cpp \ util.cpp \ + verifyopaquejob.cpp \ wkdrefreshjob.cpp # If you add one here make sure that you also add one in camelcase @@ -232,6 +233,7 @@ private_qgpgme_headers = \ signjob_p.h \ threadedjobmixin.h \ util.h \ + verifyopaquejob_p.h \ wkdrefreshjob_p.h qgpgme_moc_sources = \ diff --git a/lang/qt/src/job.cpp b/lang/qt/src/job.cpp index 56088ce3..dbaba195 100644 --- a/lang/qt/src/job.cpp +++ b/lang/qt/src/job.cpp @@ -46,7 +46,6 @@ #include "decryptjob.h" #include "signkeyjob.h" #include "verifydetachedjob.h" -#include "verifyopaquejob.h" #include "keygenerationjob.h" #include "importjob.h" #include "importfromkeyserverjob.h" @@ -163,7 +162,6 @@ make_job_subclass(ListAllKeysJob) make_job_subclass(DecryptJob) make_job_subclass(SignKeyJob) make_job_subclass(VerifyDetachedJob) -make_job_subclass(VerifyOpaqueJob) make_job_subclass(KeyGenerationJob) make_job_subclass(AbstractImportJob) make_job_subclass_ext(ImportJob, AbstractImportJob) @@ -197,7 +195,6 @@ make_job_subclass(SetPrimaryUserIDJob) #include "decryptjob.moc" #include "signkeyjob.moc" #include "verifydetachedjob.moc" -#include "verifyopaquejob.moc" #include "keygenerationjob.moc" #include "abstractimportjob.moc" #include "importjob.moc" diff --git a/lang/qt/src/qgpgmeverifyopaquejob.cpp b/lang/qt/src/qgpgmeverifyopaquejob.cpp index 01372e07..2ff53227 100644 --- a/lang/qt/src/qgpgmeverifyopaquejob.cpp +++ b/lang/qt/src/qgpgmeverifyopaquejob.cpp @@ -39,22 +39,51 @@ #include "qgpgmeverifyopaquejob.h" #include "dataprovider.h" +#include "util.h" +#include "verifyopaquejob_p.h" -#include "context.h" -#include "verificationresult.h" -#include "data.h" +#include +#include +#include #include - +#include #include using namespace QGpgME; using namespace GpgME; +namespace +{ + +class QGpgMEVerifyOpaqueJobPrivate : public VerifyOpaqueJobPrivate +{ + QGpgMEVerifyOpaqueJob *q = nullptr; + +public: + QGpgMEVerifyOpaqueJobPrivate(QGpgMEVerifyOpaqueJob *qq) + : q{qq} + { + } + + ~QGpgMEVerifyOpaqueJobPrivate() override = default; + +private: + GpgME::Error startIt() override; + + void startNow() override + { + q->run(); + } +}; + +} + QGpgMEVerifyOpaqueJob::QGpgMEVerifyOpaqueJob(Context *context) : mixin_type(context) { + setJobPrivate(this, std::unique_ptr{new QGpgMEVerifyOpaqueJobPrivate{this}}); lateInitialization(); } @@ -105,6 +134,41 @@ static QGpgMEVerifyOpaqueJob::result_type verify_opaque_qba(Context *ctx, const return verify_opaque(ctx, nullptr, buffer, std::shared_ptr()); } +static QGpgMEVerifyOpaqueJob::result_type verify_from_filename(Context *ctx, + const QString &inputFilePath, + const QString &outputFilePath) +{ + Data indata; +#ifdef Q_OS_WIN + indata.setFileName(inputFilePath().toUtf8().constData()); +#else + indata.setFileName(QFile::encodeName(inputFilePath).constData()); +#endif + + PartialFileGuard partFileGuard{outputFilePath}; + if (partFileGuard.tempFileName().isEmpty()) { + return std::make_tuple(VerificationResult{Error::fromCode(GPG_ERR_EEXIST)}, QByteArray{}, QString{}, Error{}); + } + + Data outdata; +#ifdef Q_OS_WIN + outdata.setFileName(partFileGuard.tempFileName().toUtf8().constData()); +#else + outdata.setFileName(QFile::encodeName(partFileGuard.tempFileName()).constData()); +#endif + + const auto verificationResult = ctx->verifyOpaqueSignature(indata, outdata); + + if (!verificationResult.error().code()) { + // the operation succeeded -> save the result under the requested file name + partFileGuard.commit(); + } + + Error ae; + const QString log = _detail::audit_log_as_html(ctx, ae); + return std::make_tuple(verificationResult, QByteArray{}, log, ae); +} + Error QGpgMEVerifyOpaqueJob::start(const QByteArray &signedData) { run(std::bind(&verify_opaque_qba, std::placeholders::_1, signedData)); @@ -124,10 +188,22 @@ GpgME::VerificationResult QGpgME::QGpgMEVerifyOpaqueJob::exec(const QByteArray & return mResult; } -//PENDING(marc) implement showErrorDialog() - void QGpgME::QGpgMEVerifyOpaqueJob::resultHook(const result_type &tuple) { mResult = std::get<0>(tuple); } + +GpgME::Error QGpgMEVerifyOpaqueJobPrivate::startIt() +{ + if (m_inputFilePath.isEmpty() || m_outputFilePath.isEmpty()) { + return Error::fromCode(GPG_ERR_INV_VALUE); + } + + q->run([=](Context *ctx) { + return verify_from_filename(ctx, m_inputFilePath, m_outputFilePath); + }); + + return {}; +} + #include "qgpgmeverifyopaquejob.moc" diff --git a/lang/qt/src/verifyopaquejob.cpp b/lang/qt/src/verifyopaquejob.cpp new file mode 100644 index 00000000..1ae8e75b --- /dev/null +++ b/lang/qt/src/verifyopaquejob.cpp @@ -0,0 +1,74 @@ +/* + verifyopaquejob.cpp + + This file is part of qgpgme, the Qt API binding for gpgme + Copyright (c) 2023 g10 Code GmbH + Software engineering by Ingo Klöcker + + QGpgME is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. + + QGpgME is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#ifdef HAVE_CONFIG_H + #include "config.h" +#endif + +#include "verifyopaquejob.h" +#include "verifyopaquejob_p.h" + +using namespace QGpgME; + +VerifyOpaqueJob::VerifyOpaqueJob(QObject *parent) + : Job{parent} +{ +} + +VerifyOpaqueJob::~VerifyOpaqueJob() = default; + +void VerifyOpaqueJob::setInputFile(const QString &path) +{ + auto d = jobPrivate(this); + d->m_inputFilePath = path; +} + +QString VerifyOpaqueJob::inputFile() const +{ + auto d = jobPrivate(this); + return d->m_inputFilePath; +} + +void VerifyOpaqueJob::setOutputFile(const QString &path) +{ + auto d = jobPrivate(this); + d->m_outputFilePath = path; +} + +QString VerifyOpaqueJob::outputFile() const +{ + auto d = jobPrivate(this); + return d->m_outputFilePath; +} + +#include "verifyopaquejob.moc" diff --git a/lang/qt/src/verifyopaquejob.h b/lang/qt/src/verifyopaquejob.h index c9b2247b..8dd73141 100644 --- a/lang/qt/src/verifyopaquejob.h +++ b/lang/qt/src/verifyopaquejob.h @@ -61,6 +61,14 @@ namespace QGpgME VerifyOpaqueJob instance will have scheduled it's own destruction with a call to QObject::deleteLater(). + Alternatively, the job can be started with startIt() after setting + an input file and an output file. If the job is started this way then + the backend reads the input and writes the output directly from/to the + specified input file and output file. In this case the plainText value of + the result signal will always be empty. This direct IO mode is currently + only supported for OpenPGP. Note that startIt() does not schedule the job's + destruction if starting the job failed. + After result() is emitted, the VerifyOpaqueJob will schedule it's own destruction by calling QObject::deleteLater(). */ @@ -70,7 +78,27 @@ class QGPGME_EXPORT VerifyOpaqueJob : public Job protected: explicit VerifyOpaqueJob(QObject *parent); public: - ~VerifyOpaqueJob(); + ~VerifyOpaqueJob() override; + + /** + * Sets the path of the file to verify. + * + * Used if the job is started with startIt(). + */ + void setInputFile(const QString &path); + QString inputFile() const; + + /** + * Sets the path of the file to write the result to. + * + * Used if the job is started with startIt(). + * + * \note If a file with this path exists, then the job will fail, i.e. you + * need to delete an existing file that shall be overwritten before you + * start the job. + */ + void setOutputFile(const QString &path); + QString outputFile() const; /** Starts the verification operation. \a signature contains the diff --git a/lang/qt/src/verifyopaquejob_p.h b/lang/qt/src/verifyopaquejob_p.h new file mode 100644 index 00000000..3dce6dec --- /dev/null +++ b/lang/qt/src/verifyopaquejob_p.h @@ -0,0 +1,50 @@ +/* + verifyopaquejob_p.h + + This file is part of qgpgme, the Qt API binding for gpgme + Copyright (c) 2023 g10 Code GmbH + Software engineering by Ingo Klöcker + + QGpgME is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. + + QGpgME is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#ifndef __QGPGME_VERIFYOPAQUEJOB_P_H__ +#define __QGPGME_VERIFYOPAQUEJOB_P_H__ + +#include "job_p.h" + +namespace QGpgME +{ + +struct VerifyOpaqueJobPrivate : public JobPrivate +{ + QString m_inputFilePath; + QString m_outputFilePath; +}; + +} + +#endif // __QGPGME_VERIFYOPAQUEJOB_P_H__ diff --git a/lang/qt/tests/Makefile.am b/lang/qt/tests/Makefile.am index df9c905f..e3dec517 100644 --- a/lang/qt/tests/Makefile.am +++ b/lang/qt/tests/Makefile.am @@ -100,6 +100,7 @@ run_receivekeysjob_SOURCES = run-receivekeysjob.cpp run_refreshkeysjob_SOURCES = run-refreshkeysjob.cpp run_signarchivejob_SOURCES = run-signarchivejob.cpp run_signjob_SOURCES = run-signjob.cpp +run_verifyopaquejob_SOURCES = run-verifyopaquejob.cpp run_wkdrefreshjob_SOURCES = run-wkdrefreshjob.cpp nodist_t_keylist_SOURCES = $(moc_files) @@ -120,6 +121,7 @@ noinst_PROGRAMS = \ run-importjob run-exportjob run-receivekeysjob run-refreshkeysjob \ run-signarchivejob \ run-signjob \ + run-verifyopaquejob \ run-wkdrefreshjob diff --git a/lang/qt/tests/run-verifyopaquejob.cpp b/lang/qt/tests/run-verifyopaquejob.cpp new file mode 100644 index 00000000..37466bd3 --- /dev/null +++ b/lang/qt/tests/run-verifyopaquejob.cpp @@ -0,0 +1,160 @@ +/* + run-verifyopaquejob.cpp + + This file is part of QGpgME's test suite. + Copyright (c) 2023 by g10 Code GmbH + Software engineering by Ingo Klöcker + + QGpgME is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License, + version 2, as published by the Free Software Foundation. + + QGpgME is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#ifdef HAVE_CONFIG_H + #include "config.h" +#endif + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +using namespace GpgME; + +std::ostream &operator<<(std::ostream &os, const QString &s) +{ + return os << s.toLocal8Bit().constData(); +} + +struct CommandLineOptions { + QString inputFile; + QString outputFile; + std::chrono::seconds cancelTimeout{0}; +}; + +CommandLineOptions parseCommandLine(const QStringList &arguments) +{ + CommandLineOptions options; + + QCommandLineParser parser; + parser.setApplicationDescription("Test program for VerifyOpaqueJob"); + parser.addHelpOption(); + parser.addOptions({ + {{"o", "output"}, "Write output to FILE.", "FILE"}, + {"cancel-after", "Cancel the running job after SECONDS seconds.", "SECONDS"}, + }); + parser.addPositionalArgument("file", "File to verify", "FILE"); + + parser.process(arguments); + + const auto args = parser.positionalArguments(); + if (args.size() != 1) { + parser.showHelp(1); + } + + options.inputFile = args.front(); + options.outputFile = parser.value("output"); + if (parser.isSet("cancel-after")) { + bool ok; + options.cancelTimeout = std::chrono::seconds{parser.value("cancel-after").toInt(&ok)}; + if (!ok) { + options.cancelTimeout = std::chrono::seconds{-1}; + } + } + + return options; +} + +int main(int argc, char **argv) +{ + GpgME::initializeLibrary(); + + QCoreApplication app{argc, argv}; + app.setApplicationName("run-verifyopaquejob"); + + const auto options = parseCommandLine(app.arguments()); + if (options.cancelTimeout.count() < 0) { + std::cerr << "Ignoring invalid timeout for cancel." << std::endl; + } + + std::shared_ptr output; + if (options.outputFile.isEmpty() || options.outputFile == QLatin1String{"-"}) { + output.reset(new QFile); + output->open(stdout, QIODevice::WriteOnly); + } else { + if (QFile::exists(options.outputFile)) { + qCritical() << "File" << options.outputFile << "exists. Bailing out."; + return 1; + } + } + + auto job = QGpgME::openpgp()->verifyOpaqueJob(); + if (!job) { + std::cerr << "Error: Could not create job" << std::endl; + return 1; + } + QObject::connect(job, + &QGpgME::VerifyOpaqueJob::result, + &app, + [](const GpgME::VerificationResult &verificationResult, + const QByteArray &, + const QString &auditLog, + const GpgME::Error &) { + std::cerr << "Diagnostics: " << auditLog << std::endl; + std::cerr << "Verification Result: " << verificationResult << std::endl; + qApp->quit(); + }); + if (options.cancelTimeout.count() > 0) { + QTimer::singleShot(options.cancelTimeout, job, [job]() { + std::cerr << "Canceling job" << std::endl; + job->slotCancel(); + }); + } + + std::shared_ptr input; + GpgME::Error err; + if (output) { + input.reset(new QFile{options.inputFile}); + input->open(QIODevice::ReadOnly); + job->start(input, output); + } else { + job->setInputFile(options.inputFile); + job->setOutputFile(options.outputFile); + err = job->startIt(); + } + if (err) { + std::cerr << "Error: Starting the job failed: " << err.asString() << std::endl; + return 1; + } + + return app.exec(); +}