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
@@ -1,436 +1,438 @@
 # Makefile.am for GPGMEPP.
 # Copyright (C) 2016 Bundesamt für Sicherheit in der Informationstechnik
 # Software engineering by Intevation GmbH
 #
 # This file is part of GPGMEPP.
 #
 # GPGME-CL 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.
 #
 # GPGME-CL 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 Lesser 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., 59 Temple Place - Suite 330, Boston, MA
 # 02111-1307, USA
 
 if WANT_QT5
 lib_LTLIBRARIES = libqgpgme.la
 endif
 if WANT_QT6
 lib_LTLIBRARIES = libqgpgmeqt6.la
 endif
 
 EXTRA_DIST = QGpgmeConfig.cmake.in.in QGpgmeConfigVersion.cmake.in \
              qgpgme_debug.h qgpgme_version.h.in \
              QGpgmeConfig-w32.cmake.in.in QGpgmeQt6Config.cmake.in.in \
              QGpgmeQt6Config-w32.cmake.in.in QGpgmeQt6ConfigVersion.cmake.in
 
 qgpgme_sources = \
     cleaner.cpp \
     dataprovider.cpp \
     debug.cpp \
     decryptverifyarchivejob.cpp \
     decryptverifyjob.cpp \
     encryptarchivejob.cpp \
     filelistdataprovider.cpp \
     job.cpp multideletejob.cpp qgpgmeadduseridjob.cpp \
     qgpgmeaddexistingsubkeyjob.cpp \
     qgpgmebackend.cpp qgpgmechangeexpiryjob.cpp qgpgmechangeownertrustjob.cpp \
     qgpgmechangepasswdjob.cpp qgpgmedecryptjob.cpp \
     qgpgmedecryptverifyarchivejob.cpp \
     qgpgmedecryptverifyjob.cpp qgpgmedeletejob.cpp qgpgmedownloadjob.cpp \
     qgpgmeencryptarchivejob.cpp \
     qgpgmeencryptjob.cpp qgpgmeexportjob.cpp qgpgmeimportfromkeyserverjob.cpp \
     qgpgmeimportjob.cpp qgpgmekeygenerationjob.cpp qgpgmekeylistjob.cpp \
     listallkeysjob.cpp qgpgmelistallkeysjob.cpp qgpgmenewcryptoconfig.cpp \
     qgpgmereceivekeysjob.cpp \
     qgpgmerefreshsmimekeysjob.cpp \
     qgpgmerevokekeyjob.cpp \
     qgpgmesetprimaryuseridjob.cpp \
     qgpgmesignarchivejob.cpp \
     qgpgmesignencryptjob.cpp \
     qgpgmesignencryptarchivejob.cpp \
     qgpgmesignjob.cpp qgpgmesignkeyjob.cpp qgpgmeverifydetachedjob.cpp \
     qgpgmeverifyopaquejob.cpp qgpgmewkdlookupjob.cpp threadedjobmixin.cpp \
     qgpgmewkdrefreshjob.cpp \
     qgpgmekeyformailboxjob.cpp qgpgme_debug.cpp \
     qgpgmetofupolicyjob.cpp qgpgmequickjob.cpp \
     defaultkeygenerationjob.cpp qgpgmewkspublishjob.cpp \
     qgpgmegpgcardjob.cpp changeexpiryjob.cpp encryptjob.cpp importjob.cpp \
     signarchivejob.cpp \
     signencryptjob.cpp \
     signencryptarchivejob.cpp \
     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
 qgpgme_headers= \
     abstractimportjob.h \
     addexistingsubkeyjob.h \
     adduseridjob.h \
     changeexpiryjob.h \
     changeownertrustjob.h \
     changepasswdjob.h \
     dataprovider.h \
     debug.h \
     decryptjob.h \
     decryptverifyarchivejob.h \
     decryptverifyjob.h \
     downloadjob.h \
     encryptarchivejob.h \
     encryptjob.h \
     exportjob.h \
     filelistdataprovider.h \
     hierarchicalkeylistjob.h \
     job.h \
     keyformailboxjob.h \
     multideletejob.h \
     protocol.h \
     qgpgme_export.h \
     qgpgmenewcryptoconfig.h \
     quickjob.h \
     receivekeysjob.h \
     revokekeyjob.h \
     setprimaryuseridjob.h \
     specialjob.h \
     signarchivejob.h \
     signjob.h \
     signkeyjob.h \
     signencryptjob.h \
     signencryptarchivejob.h \
     verifyopaquejob.h \
     refreshkeysjob.h \
     cryptoconfig.h \
     deletejob.h \
     importfromkeyserverjob.h \
     importjob.h \
     keygenerationjob.h \
     keylistjob.h \
     listallkeysjob.h \
     verifydetachedjob.h \
     defaultkeygenerationjob.h \
     tofupolicyjob.h \
     wkdlookupjob.h \
     wkdlookupresult.h \
     wkdrefreshjob.h \
     wkspublishjob.h \
     gpgcardjob.h \
     dn.h
 
 camelcase_headers= \
     AddExistingSubkeyJob \
     AddUserIDJob \
     AbstractImportJob \
     ChangeExpiryJob \
     ChangeOwnerTrustJob \
     ChangePasswdJob \
     DataProvider \
     Debug \
     DecryptJob \
     DecryptVerifyArchiveJob \
     DecryptVerifyJob \
     DN \
     DownloadJob \
     EncryptArchiveJob \
     EncryptJob \
     ExportJob \
     FileListDataProvider \
     HierarchicalKeyKistJob \
     Job \
     MultiDeleteJob \
     Protocol \
     QGpgMENewCryptoConfig \
     QuickJob \
     ReceiveKeysJob \
     RevokeKeyJob \
     SetPrimaryUserIDJob \
     SpecialJob \
     SignArchiveJob \
     SignJob \
     SignKeyJob \
     SignEncryptArchiveJob \
     SignEncryptJob \
     VerifyOpaqueJob \
     RefreshKeysJob \
     CryptoConfig \
     DeleteJob \
     ImportFromKeyserverJob \
     ImportJob \
     KeyGenerationJob \
     KeyListJob \
     ListAllKeysJob \
     VerifyDetachedJob \
     KeyForMailboxJob \
     DefaultKeyGenerationJob \
     WKDLookupJob \
     WKDLookupResult \
     WKDRefreshJob \
     WKSPublishJob \
     TofuPolicyJob \
     GpgCardJob
 
 private_qgpgme_headers = \
     changeexpiryjob_p.h \
     cleaner.h \
     decryptverifyarchivejob_p.h \
     decryptverifyjob_p.h \
     encryptarchivejob_p.h \
     encryptjob_p.h \
     importjob_p.h \
     listallkeysjob_p.h \
     qgpgme_export.h \
     protocol_p.h \
     job_p.h \
     qgpgmeaddexistingsubkeyjob.h \
     qgpgmeadduseridjob.h \
     qgpgmebackend.h \
     qgpgmechangeexpiryjob.h \
     qgpgmechangeownertrustjob.h \
     qgpgmechangepasswdjob.h \
     qgpgmedecryptjob.h \
     qgpgmedecryptverifyarchivejob.h \
     qgpgmedecryptverifyjob.h \
     qgpgmedeletejob.h \
     qgpgmedownloadjob.h \
     qgpgmeencryptarchivejob.h \
     qgpgmeencryptjob.h \
     qgpgmeexportjob.h \
     qgpgmeimportfromkeyserverjob.h \
     qgpgmeimportjob.h \
     qgpgmekeygenerationjob.h \
     qgpgmekeylistjob.h \
     qgpgmelistallkeysjob.h \
     qgpgmereceivekeysjob.h \
     qgpgmerefreshsmimekeysjob.h \
     qgpgmerevokekeyjob.h \
     qgpgmesetprimaryuseridjob.h \
     qgpgmesignarchivejob.h \
     qgpgmesignencryptjob.h \
     qgpgmesignencryptarchivejob.h \
     qgpgmesignjob.h \
     qgpgmesignkeyjob.h \
     qgpgmeverifydetachedjob.h \
     qgpgmeverifyopaquejob.h \
     qgpgmewkdlookupjob.h \
     qgpgmewkdrefreshjob.h \
     qgpgmekeyformailboxjob.h \
     qgpgmewkspublishjob.h \
     qgpgmetofupolicyjob.h \
     qgpgmegpgcardjob.h \
     qgpgmequickjob.h \
     signarchivejob_p.h \
     signencryptjob_p.h \
     signencryptarchivejob_p.h \
     signjob_p.h \
     threadedjobmixin.h \
     util.h \
+    verifyopaquejob_p.h \
     wkdrefreshjob_p.h
 
 qgpgme_moc_sources = \
     abstractimportjob.moc \
     addexistingsubkeyjob.moc \
     adduseridjob.moc \
     changeexpiryjob.moc \
     changeownertrustjob.moc \
     changepasswdjob.moc \
     cleaner.moc \
     decryptjob.moc \
     decryptverifyarchivejob.moc \
     decryptverifyjob.moc \
     deletejob.moc \
     downloadjob.moc \
     encryptarchivejob.moc \
     encryptjob.moc \
     exportjob.moc \
     hierarchicalkeylistjob.moc \
     importfromkeyserverjob.moc \
     importjob.moc \
     job.moc \
     keygenerationjob.moc \
     keylistjob.moc \
     listallkeysjob.moc \
     multideletejob.moc \
     qgpgmeaddexistingsubkeyjob.moc \
     qgpgmeadduseridjob.moc \
     qgpgmechangeexpiryjob.moc \
     qgpgmechangeownertrustjob.moc \
     qgpgmechangepasswdjob.moc \
     qgpgmedecryptjob.moc \
     qgpgmedecryptverifyarchivejob.moc \
     qgpgmedecryptverifyjob.moc \
     qgpgmedeletejob.moc \
     qgpgmedownloadjob.moc \
     qgpgmeencryptarchivejob.moc \
     qgpgmeencryptjob.moc \
     qgpgmeexportjob.moc \
     qgpgmeimportfromkeyserverjob.moc \
     qgpgmeimportjob.moc \
     qgpgmekeygenerationjob.moc \
     qgpgmekeylistjob.moc \
     qgpgmelistallkeysjob.moc \
     qgpgmereceivekeysjob.moc \
     qgpgmerefreshsmimekeysjob.moc \
     qgpgmerevokekeyjob.moc \
     qgpgmesetprimaryuseridjob.moc \
     qgpgmesignarchivejob.moc \
     qgpgmesignencryptjob.moc \
     qgpgmesignencryptarchivejob.moc \
     qgpgmesignjob.moc \
     qgpgmesignkeyjob.moc \
     qgpgmeverifydetachedjob.moc \
     qgpgmeverifyopaquejob.moc \
     qgpgmewkdlookupjob.moc \
     qgpgmewkdrefreshjob.moc \
     qgpgmewkspublishjob.moc \
     tofupolicyjob.moc \
     qgpgmetofupolicyjob.moc \
     receivekeysjob.moc \
     refreshkeysjob.moc \
     revokekeyjob.moc \
     setprimaryuseridjob.moc \
     signarchivejob.moc \
     signencryptjob.moc \
     signencryptarchivejob.moc \
     signjob.moc \
     signkeyjob.moc \
     specialjob.moc \
     verifydetachedjob.moc \
     verifyopaquejob.moc \
     wkdlookupjob.moc \
     wkdrefreshjob.moc \
     keyformailboxjob.moc \
     wkspublishjob.moc \
     qgpgmekeyformailboxjob.moc \
     defaultkeygenerationjob.moc \
     quickjob.moc \
     qgpgmequickjob.moc \
     gpgcardjob.moc \
     qgpgmegpgcardjob.moc
 
 qgpgmeincludedir = $(includedir)/qgpgme
 qgpgmeinclude_HEADERS = $(qgpgme_headers)
 camelcaseincludedir = $(includedir)/QGpgME
 camelcaseinclude_HEADERS = $(camelcase_headers)
 nodist_qgpgmeinclude_HEADERS = qgpgme_version.h
 
 if WANT_QT5
 AM_CPPFLAGS = -I$(top_srcdir)/lang/cpp/src -I$(top_builddir)/src \
               @GPGME_QT5_CFLAGS@ @GPG_ERROR_CFLAGS@ @LIBASSUAN_CFLAGS@ \
               -DBUILDING_QGPGME -Wsuggest-override \
               -Wzero-as-null-pointer-constant
 
 libqgpgme_la_SOURCES = $(qgpgme_sources) $(qgpgme_headers) $(private_qgpgme_headers)
 
 libqgpgme_la_LIBADD = ../../cpp/src/libgpgmepp.la ../../../src/libgpgme.la \
                       @LIBASSUAN_LIBS@ @GPG_ERROR_LIBS@ @GPGME_QT5_LIBS@
 libqgpgme_la_LDFLAGS = -no-undefined -version-info \
                        @LIBQGPGME_LT_CURRENT@:@LIBQGPGME_LT_REVISION@:@LIBQGPGME_LT_AGE@
 endif
 if WANT_QT6
 AM_CPPFLAGS = -I$(top_srcdir)/lang/cpp/src -I$(top_builddir)/src \
               @GPGME_QT6_CFLAGS@ @GPG_ERROR_CFLAGS@ @LIBASSUAN_CFLAGS@ \
               -DBUILDING_QGPGME -Wsuggest-override \
               -Wzero-as-null-pointer-constant
 
 libqgpgmeqt6_la_SOURCES = $(qgpgme_sources) $(qgpgme_headers) $(private_qgpgme_headers)
 
 libqgpgmeqt6_la_LIBADD = ../../cpp/src/libgpgmepp.la ../../../src/libgpgme.la \
                          @LIBASSUAN_LIBS@ @GPG_ERROR_LIBS@ @GPGME_QT6_LIBS@
 libqgpgmeqt6_la_LDFLAGS = -no-undefined -version-info \
                           @LIBQGPGME_LT_CURRENT@:@LIBQGPGME_LT_REVISION@:@LIBQGPGME_LT_AGE@
 endif
 
 if HAVE_MACOS_SYSTEM
 libsuffix=.dylib
 else
 libsuffix=.so
 endif
 
 if WANT_QT5
 if HAVE_W32_SYSTEM
 QGpgmeConfig.cmake: QGpgmeConfig-w32.cmake.in
 	sed -e 's|[@]resolved_bindir@|$(bindir)|g' < "$<" | \
 	sed -e 's|[@]resolved_libdir@|$(libdir)|g' | \
 	sed -e 's|[@]resolved_includedir@|$(includedir)|g' > $@
 else
 QGpgmeConfig.cmake: QGpgmeConfig.cmake.in
 	sed -e 's|[@]resolved_libdir@|$(libdir)|g' < "$<" | \
 	sed -e 's|[@]libsuffix@|$(libsuffix)|g' | \
 	sed -e 's|[@]resolved_includedir@|$(includedir)|g' > $@
 endif
 endif
 if WANT_QT6
 if HAVE_W32_SYSTEM
 QGpgmeQt6Config.cmake: QGpgmeQt6Config-w32.cmake.in
 	sed -e 's|[@]resolved_bindir@|$(bindir)|g' < "$<" | \
 	sed -e 's|[@]resolved_libdir@|$(libdir)|g' | \
 	sed -e 's|[@]resolved_includedir@|$(includedir)|g' > $@
 else
 QGpgmeQt6Config.cmake: QGpgmeQt6Config.cmake.in
 	sed -e 's|[@]resolved_libdir@|$(libdir)|g' < "$<" | \
 	sed -e 's|[@]libsuffix@|$(libsuffix)|g' | \
 	sed -e 's|[@]resolved_includedir@|$(includedir)|g' > $@
 endif
 endif
 
 $(camelcase_headers): Makefile.am
 	echo -n "#include \"qgpgme/" > "$@"
 	echo -n "$@" | tr '[:upper:]' '[:lower:]' >> "$@"
 	echo ".h\"" >> "$@"
 
 if WANT_QT5
 install-cmake-files: QGpgmeConfig.cmake QGpgmeConfigVersion.cmake
 	-$(INSTALL) -d $(DESTDIR)$(libdir)/cmake/QGpgme
 	$(INSTALL) -m 644 QGpgmeConfig.cmake \
 	$(DESTDIR)$(libdir)/cmake/QGpgme/QGpgmeConfig.cmake
 	$(INSTALL) -m 644 QGpgmeConfigVersion.cmake \
 	$(DESTDIR)$(libdir)/cmake/QGpgme/QGpgmeConfigVersion.cmake
 
 uninstall-cmake-files:
 	-rm $(DESTDIR)$(libdir)/cmake/QGpgme/QGpgmeConfigVersion.cmake
 	-rm $(DESTDIR)$(libdir)/cmake/QGpgme/QGpgmeConfig.cmake
 	-rmdir $(DESTDIR)$(libdir)/cmake/QGpgme/
 endif
 if WANT_QT6
 install-cmake-files: QGpgmeQt6Config.cmake QGpgmeQt6ConfigVersion.cmake
 	-$(INSTALL) -d $(DESTDIR)$(libdir)/cmake/QGpgmeQt6
 	$(INSTALL) -m 644 QGpgmeQt6Config.cmake \
 	$(DESTDIR)$(libdir)/cmake/QGpgmeQt6/QGpgmeQt6Config.cmake
 	$(INSTALL) -m 644 QGpgmeQt6ConfigVersion.cmake \
 	$(DESTDIR)$(libdir)/cmake/QGpgmeQt6/QGpgmeQt6ConfigVersion.cmake
 
 uninstall-cmake-files:
 	-rm $(DESTDIR)$(libdir)/cmake/QGpgmeQt6/QGpgmeQt6ConfigVersion.cmake
 	-rm $(DESTDIR)$(libdir)/cmake/QGpgmeQt6/QGpgmeQt6Config.cmake
 	-rmdir $(DESTDIR)$(libdir)/cmake/QGpgmeQt6/
 endif
 
 install-data-local: install-cmake-files
 
 uninstall-local: uninstall-cmake-files
 
 BUILT_SOURCES = $(qgpgme_moc_sources) $(camelcase_headers)
 
 CLEANFILES = $(qgpgme_moc_sources) $(camelcase_headers) QGpgmeConfig.cmake \
              qgpgme_version.h QGpgmeConfig.cmake.in \
              QGpgmeConfig-w32.cmake.in QGpgmeConfigVersion.cmake \
              QGpgmeQt6Config.cmake.in QGpgmeQt6Config-w32.cmake.in \
              QGpgmeQt6Config.cmake QGpgmeQt6ConfigVersion.cmake
 
 if WANT_QT5
 nodist_libqgpgme_la_SOURCES = $(qgpgme_moc_sources)
 endif
 if WANT_QT6
 nodist_libqgpgmeqt6_la_SOURCES = $(qgpgme_moc_sources)
 endif
 
 .h.moc:
 	$(MOC) `test -f '$<' || echo '$(srcdir)/'`$< -o $@
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
@@ -1,223 +1,220 @@
 /*
     job.cpp
 
     This file is part of qgpgme, the Qt API binding for gpgme
     Copyright (c) 2004,2005 Klarälvdalens Datakonsult AB
     Copyright (c) 2016 by Bundesamt für Sicherheit in der Informationstechnik
     Software engineering by Intevation GmbH
     Copyright (c) 2021 g10 Code GmbH
     Software engineering by Ingo Klöcker <dev@ingo-kloecker.de>
 
     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 "job.h"
 #include "job_p.h"
 
 #include "keylistjob.h"
 #include "listallkeysjob.h"
 #include "decryptjob.h"
 #include "signkeyjob.h"
 #include "verifydetachedjob.h"
-#include "verifyopaquejob.h"
 #include "keygenerationjob.h"
 #include "importjob.h"
 #include "importfromkeyserverjob.h"
 #include "exportjob.h"
 #include "changeexpiryjob.h"
 #include "changeownertrustjob.h"
 #include "changepasswdjob.h"
 #include "downloadjob.h"
 #include "deletejob.h"
 #include "refreshkeysjob.h"
 #include "addexistingsubkeyjob.h"
 #include "adduseridjob.h"
 #include "specialjob.h"
 #include "keyformailboxjob.h"
 #include "wkdlookupjob.h"
 #include "wkspublishjob.h"
 #include "tofupolicyjob.h"
 #include "threadedjobmixin.h"
 #include "quickjob.h"
 #include "gpgcardjob.h"
 #include "receivekeysjob.h"
 #include "revokekeyjob.h"
 #include "setprimaryuseridjob.h"
 
 #include <QCoreApplication>
 #include <QDebug>
 
 #include <gpg-error.h>
 
 #include <unordered_map>
 
 namespace
 {
 typedef std::unordered_map<const QGpgME::Job*, std::unique_ptr<QGpgME::JobPrivate>> JobPrivateHash;
 Q_GLOBAL_STATIC(JobPrivateHash, d_func)
 }
 
 void QGpgME::setJobPrivate(const Job *job, std::unique_ptr<JobPrivate> d)
 {
     auto &ref = d_func()->operator[](job);
     ref = std::move(d);
 }
 
 const QGpgME::JobPrivate *QGpgME::getJobPrivate(const Job *job)
 {
     return d_func()->operator[](job).get();
 }
 
 QGpgME::JobPrivate *QGpgME::getJobPrivate(Job *job)
 {
     return d_func()->operator[](job).get();
 }
 
 QGpgME::Job::Job(QObject *parent)
     : QObject(parent)
 {
     if (QCoreApplication *app = QCoreApplication::instance()) {
         connect(app, &QCoreApplication::aboutToQuit, this, &Job::slotCancel);
     }
 }
 
 QGpgME::Job::~Job()
 {
     ::d_func()->erase(this);
 }
 
 QString QGpgME::Job::auditLogAsHtml() const
 {
     qDebug() << "QGpgME::Job::auditLogAsHtml() should be reimplemented in Kleo::Job subclasses!";
     return QString();
 }
 
 GpgME::Error QGpgME::Job::auditLogError() const
 {
     qDebug() << "QGpgME::Job::auditLogError() should be reimplemented in Kleo::Job subclasses!";
     return GpgME::Error::fromCode(GPG_ERR_NOT_IMPLEMENTED);
 }
 
 bool QGpgME::Job::isAuditLogSupported() const
 {
     return auditLogError().code() != GPG_ERR_NOT_IMPLEMENTED;
 }
 
 QMap <QGpgME::Job *, GpgME::Context *> QGpgME::g_context_map;
 
 /* static */
 GpgME::Context *QGpgME::Job::context(QGpgME::Job *job)
 {
     return QGpgME::g_context_map.value (job, nullptr);
 }
 
 GpgME::Error QGpgME::Job::startIt()
 {
     auto d = getJobPrivate(this);
     Q_ASSERT(d && "This Job class has no JobPrivate class");
     return d->startIt();
 }
 
 void QGpgME::Job::startNow()
 {
     auto d = getJobPrivate(this);
     Q_ASSERT(d && "This Job class has no JobPrivate class");
     d->startNow();
 }
 
 #define make_job_subclass_ext(x,y)                \
     QGpgME::x::x( QObject * parent ) : y( parent ) {} \
     QGpgME::x::~x() {}
 
 #define make_job_subclass(x) make_job_subclass_ext(x,Job)
 
 make_job_subclass(KeyListJob)
 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)
 make_job_subclass_ext(ImportFromKeyserverJob, AbstractImportJob)
 make_job_subclass_ext(ReceiveKeysJob, AbstractImportJob)
 make_job_subclass(ExportJob)
 make_job_subclass(ChangeExpiryJob)
 make_job_subclass(ChangeOwnerTrustJob)
 make_job_subclass(ChangePasswdJob)
 make_job_subclass(DownloadJob)
 make_job_subclass(DeleteJob)
 make_job_subclass(RefreshKeysJob)
 make_job_subclass(AddExistingSubkeyJob)
 make_job_subclass(AddUserIDJob)
 make_job_subclass(SpecialJob)
 make_job_subclass(KeyForMailboxJob)
 make_job_subclass(WKDLookupJob)
 make_job_subclass(WKSPublishJob)
 make_job_subclass(TofuPolicyJob)
 make_job_subclass(QuickJob)
 make_job_subclass(GpgCardJob)
 make_job_subclass(RevokeKeyJob)
 make_job_subclass(SetPrimaryUserIDJob)
 
 #undef make_job_subclass
 
 #include "job.moc"
 
 #include "keylistjob.moc"
 #include "listallkeysjob.moc"
 #include "decryptjob.moc"
 #include "signkeyjob.moc"
 #include "verifydetachedjob.moc"
-#include "verifyopaquejob.moc"
 #include "keygenerationjob.moc"
 #include "abstractimportjob.moc"
 #include "importjob.moc"
 #include "importfromkeyserverjob.moc"
 #include "exportjob.moc"
 #include "changeexpiryjob.moc"
 #include "changeownertrustjob.moc"
 #include "changepasswdjob.moc"
 #include "downloadjob.moc"
 #include "deletejob.moc"
 #include "refreshkeysjob.moc"
 #include "addexistingsubkeyjob.moc"
 #include "adduseridjob.moc"
 #include "specialjob.moc"
 #include "keyformailboxjob.moc"
 #include "wkdlookupjob.moc"
 #include "wkspublishjob.moc"
 #include "tofupolicyjob.moc"
 #include "quickjob.moc"
 #include "gpgcardjob.moc"
 #include "receivekeysjob.moc"
 #include "revokekeyjob.moc"
 #include "setprimaryuseridjob.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
@@ -1,133 +1,209 @@
 /*
     qgpgmeverifyopaquejob.cpp
 
     This file is part of qgpgme, the Qt API binding for gpgme
     Copyright (c) 2004,2007,2008 Klarälvdalens Datakonsult AB
     Copyright (c) 2016 by Bundesamt für Sicherheit in der Informationstechnik
     Software engineering by Intevation GmbH
 
     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 "qgpgmeverifyopaquejob.h"
 
 #include "dataprovider.h"
+#include "util.h"
+#include "verifyopaquejob_p.h"
 
-#include "context.h"
-#include "verificationresult.h"
-#include "data.h"
+#include <context.h>
+#include <data.h>
+#include <verificationresult.h>
 
 #include <QBuffer>
-
+#include <QFile>
 
 #include <cassert>
 
 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<QGpgMEVerifyOpaqueJobPrivate>{new QGpgMEVerifyOpaqueJobPrivate{this}});
     lateInitialization();
 }
 
 QGpgMEVerifyOpaqueJob::~QGpgMEVerifyOpaqueJob() {}
 
 static QGpgMEVerifyOpaqueJob::result_type verify_opaque(Context *ctx, QThread *thread, const std::weak_ptr<QIODevice> &signedData_, const std::weak_ptr<QIODevice> &plainText_)
 {
 
     const std::shared_ptr<QIODevice> plainText = plainText_.lock();
     const std::shared_ptr<QIODevice> signedData = signedData_.lock();
 
     const _detail::ToThreadMover ptMover(plainText,  thread);
     const _detail::ToThreadMover sdMover(signedData, thread);
 
     QGpgME::QIODeviceDataProvider in(signedData);
     Data indata(&in);
     if (!signedData->isSequential()) {
         indata.setSizeHint(signedData->size());
     }
 
     if (!plainText) {
         QGpgME::QByteArrayDataProvider out;
         Data outdata(&out);
 
         const VerificationResult res = ctx->verifyOpaqueSignature(indata, outdata);
         Error ae;
         const QString log = _detail::audit_log_as_html(ctx, ae);
         return std::make_tuple(res, out.data(), log, ae);
     } else {
         QGpgME::QIODeviceDataProvider out(plainText);
         Data outdata(&out);
 
         const VerificationResult res = ctx->verifyOpaqueSignature(indata, outdata);
         Error ae;
         const QString log = _detail::audit_log_as_html(ctx, ae);
         return std::make_tuple(res, QByteArray(), log, ae);
     }
 
 }
 
 static QGpgMEVerifyOpaqueJob::result_type verify_opaque_qba(Context *ctx, const QByteArray &signedData)
 {
     const std::shared_ptr<QBuffer> buffer(new QBuffer);
     buffer->setData(signedData);
     if (!buffer->open(QIODevice::ReadOnly)) {
         assert(!"This should never happen: QBuffer::open() failed");
     }
     return verify_opaque(ctx, nullptr, buffer, std::shared_ptr<QIODevice>());
 }
 
+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));
     return Error();
 }
 
 void QGpgMEVerifyOpaqueJob::start(const std::shared_ptr<QIODevice> &signedData, const std::shared_ptr<QIODevice> &plainText)
 {
     run(std::bind(&verify_opaque, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), signedData, plainText);
 }
 
 GpgME::VerificationResult QGpgME::QGpgMEVerifyOpaqueJob::exec(const QByteArray &signedData, QByteArray &plainText)
 {
     const result_type r = verify_opaque_qba(context(), signedData);
     plainText = std::get<1>(r);
     resultHook(r);
     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 <dev@ingo-kloecker.de>
+
+    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<VerifyOpaqueJobPrivate>(this);
+    d->m_inputFilePath = path;
+}
+
+QString VerifyOpaqueJob::inputFile() const
+{
+    auto d = jobPrivate<VerifyOpaqueJobPrivate>(this);
+    return d->m_inputFilePath;
+}
+
+void VerifyOpaqueJob::setOutputFile(const QString &path)
+{
+    auto d = jobPrivate<VerifyOpaqueJobPrivate>(this);
+    d->m_outputFilePath = path;
+}
+
+QString VerifyOpaqueJob::outputFile() const
+{
+    auto d = jobPrivate<VerifyOpaqueJobPrivate>(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
@@ -1,100 +1,128 @@
 /*
     verifyopaquejob.h
 
     This file is part of qgpgme, the Qt API binding for gpgme
     Copyright (c) 2004, 2007 Klarälvdalens Datakonsult AB
     Copyright (c) 2016 by Bundesamt für Sicherheit in der Informationstechnik
     Software engineering by Intevation GmbH
 
     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 __KLEO_VERIFYOPAQUEJOB_H__
 #define __KLEO_VERIFYOPAQUEJOB_H__
 
 #include "job.h"
 
 #include <memory>
 
 class QByteArray;
 class QIODevice;
 
 namespace GpgME
 {
 class Error;
 class VerificationResult;
 }
 
 namespace QGpgME
 {
 
 /**
    @short An abstract base class for asynchronous verification of opaque signatures
 
    To use a VerifyOpaqueJob, first obtain an instance from the
    CryptoBackend implementation, connect the progress() and result()
    signals to suitable slots and then start the verification with a
    call to start(). This call might fail, in which case the
    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().
 */
 class QGPGME_EXPORT VerifyOpaqueJob : public Job
 {
     Q_OBJECT
 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
        signature data, while \a signedData contains the data over
        which the signature was made.
     */
     virtual GpgME::Error start(const QByteArray &signedData) = 0;
 
     /*!
       \overload
 
       If \a plainText is non-null, the plaintext is written
       there. Otherwise, it will be delivered in the second argument
       of result().
     */
     virtual void start(const std::shared_ptr<QIODevice> &signedData, const std::shared_ptr<QIODevice> &plainText = std::shared_ptr<QIODevice>()) = 0;
 
     /** Synchronous version of @ref start */
     virtual GpgME::VerificationResult exec(const QByteArray &signedData, QByteArray &plainText) = 0;
 
 Q_SIGNALS:
     void result(const GpgME::VerificationResult &result, const QByteArray &plainText, const QString &auditLogAsHtml = QString(), const GpgME::Error &auditLogError = GpgME::Error());
 };
 
 }
 
 #endif // __KLEO_VERIFYOPAQUEJOB_H__
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 <dev@ingo-kloecker.de>
+
+    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
@@ -1,154 +1,156 @@
 # Makefile.am - Makefile for GPGME Qt tests.
 # Copyright (C) 2016 Bundesamt für Sicherheit in der Informationstechnik
 # Software engineering by Intevation GmbH
 #
 # This file is part of GPGME.
 #
 # GPGME is free software; you can redistribute it and/or modify it
 # under the terms of the GNU Lesser General Public License as
 # published by the Free Software Foundation; either version 2.1 of the
 # License, or (at your option) any later version.
 #
 # GPGME 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 Lesser General
 # Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public
 # License along with this program; if not, see <https://www.gnu.org/licenses/>.
 
 ## Process this file with automake to produce Makefile.in
 
 GPG = gpg
 
 GNUPGHOME=$(abs_builddir)
 TESTS_ENVIRONMENT = GNUPGHOME=$(GNUPGHOME)
 
 EXTRA_DIST = initial.test final.test
 
 the_tests = \
 	t-addexistingsubkey \
 	t-decryptverify \
 	t-keylist t-keylocate t-ownertrust t-tofuinfo \
 	t-encrypt t-verify t-various t-config t-remarks t-trustsignatures \
 	t-changeexpiryjob t-wkdlookup t-import t-revokekey t-setprimaryuserid
 
 TESTS = initial.test $(the_tests) final.test
 
 moc_files = \
 	t-addexistingsubkey.moc \
 	t-decryptverify.moc \
 	t-keylist.moc t-keylocate.moc t-ownertrust.moc t-tofuinfo.moc \
 	t-encrypt.moc t-support.hmoc t-wkspublish.moc t-verify.moc \
 	t-various.moc t-config.moc t-remarks.moc t-trustsignatures.moc \
 	t-changeexpiryjob.moc t-wkdlookup.moc t-import.moc t-revokekey.moc \
 	t-setprimaryuserid.moc
 
 AM_LDFLAGS = -no-install
 
 if WANT_QT5
 LDADD = ../../cpp/src/libgpgmepp.la ../src/libqgpgme.la \
         ../../../src/libgpgme.la @GPGME_QT5_LIBS@ @GPG_ERROR_LIBS@ \
         @GPGME_QT5TEST_LIBS@ @LDADD_FOR_TESTS_KLUDGE@ -lstdc++
 
 AM_CPPFLAGS = -I$(top_srcdir)/lang/qt/src -I$(top_srcdir)/lang/cpp/src \
               -I$(top_builddir)/src \
               @GPG_ERROR_CFLAGS@ @GPGME_QT5_CFLAGS@ @GPG_ERROR_CFLAGS@ \
               @LIBASSUAN_CFLAGS@ @GPGME_QT5TEST_CFLAGS@ -DBUILDING_QGPGME \
               -DTOP_SRCDIR="$(top_srcdir)"
 endif
 if WANT_QT6
 LDADD = ../../cpp/src/libgpgmepp.la ../src/libqgpgmeqt6.la \
         ../../../src/libgpgme.la @GPGME_QT6_LIBS@ @GPG_ERROR_LIBS@ \
         @GPGME_QT6TEST_LIBS@ @LDADD_FOR_TESTS_KLUDGE@ -lstdc++
 
 AM_CPPFLAGS = -I$(top_srcdir)/lang/qt/src -I$(top_srcdir)/lang/cpp/src \
               -I$(top_builddir)/src \
               @GPG_ERROR_CFLAGS@ @GPGME_QT6_CFLAGS@ @GPG_ERROR_CFLAGS@ \
               @LIBASSUAN_CFLAGS@ @GPGME_QT6TEST_CFLAGS@ -DBUILDING_QGPGME \
               -DTOP_SRCDIR="$(top_srcdir)"
 endif
 
 support_src = t-support.h t-support.cpp
 
 t_addexistingsubkey_SOURCES = t-addexistingsubkey.cpp $(support_src)
 t_decryptverify_SOURCES = t-decryptverify.cpp $(support_src)
 t_keylist_SOURCES = t-keylist.cpp $(support_src)
 t_keylocate_SOURCES = t-keylocate.cpp $(support_src)
 t_ownertrust_SOURCES = t-ownertrust.cpp $(support_src)
 t_tofuinfo_SOURCES = t-tofuinfo.cpp $(support_src)
 t_encrypt_SOURCES = t-encrypt.cpp $(support_src)
 t_wkspublish_SOURCES = t-wkspublish.cpp $(support_src)
 t_verify_SOURCES = t-verify.cpp $(support_src)
 t_various_SOURCES = t-various.cpp $(support_src)
 t_config_SOURCES = t-config.cpp $(support_src)
 t_remarks_SOURCES = t-remarks.cpp $(support_src)
 t_trustsignatures_SOURCES = t-trustsignatures.cpp $(support_src)
 t_changeexpiryjob_SOURCES = t-changeexpiryjob.cpp $(support_src)
 t_wkdlookup_SOURCES = t-wkdlookup.cpp $(support_src)
 t_import_SOURCES = t-import.cpp $(support_src)
 t_revokekey_SOURCES = t-revokekey.cpp $(support_src)
 t_setprimaryuserid_SOURCES = t-setprimaryuserid.cpp $(support_src)
 run_decryptverifyjob_SOURCES = run-decryptverifyjob.cpp
 run_decryptverifyarchivejob_SOURCES = run-decryptverifyarchivejob.cpp
 run_encryptarchivejob_SOURCES = run-encryptarchivejob.cpp
 run_encryptjob_SOURCES = run-encryptjob.cpp
 run_exportjob_SOURCES = run-exportjob.cpp
 run_importjob_SOURCES = run-importjob.cpp
 run_keyformailboxjob_SOURCES = run-keyformailboxjob.cpp
 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)
 
 BUILT_SOURCES = $(moc_files) pubring-stamp
 
 noinst_PROGRAMS = \
 	t-addexistingsubkey \
 	t-decryptverify \
 	t-keylist t-keylocate t-ownertrust t-tofuinfo t-encrypt \
 	run-keyformailboxjob t-wkspublish t-verify t-various t-config t-remarks \
 	t-trustsignatures t-changeexpiryjob t-wkdlookup t-import t-revokekey \
 	t-setprimaryuserid \
 	run-decryptverifyarchivejob \
 	run-decryptverifyjob \
 	run-encryptarchivejob \
 	run-encryptjob \
 	run-importjob run-exportjob run-receivekeysjob run-refreshkeysjob \
 	run-signarchivejob \
 	run-signjob \
+	run-verifyopaquejob \
 	run-wkdrefreshjob
 
 
 CLEANFILES = secring.gpg pubring.gpg pubring.kbx trustdb.gpg dirmngr.conf \
 	gpg-agent.conf pubring.kbx~ S.gpg-agent gpg.conf pubring.gpg~ \
 	random_seed S.gpg-agent .gpg-v21-migrated pubring-stamp $(moc_files) \
 	gpg.conf tofu.db reader_0.status reader_1.status
 
 clean-local:
 	-$(TESTS_ENVIRONMENT) $(top_srcdir)/tests/start-stop-agent --stop
 	-rm -fR  private-keys-v1.d crls.d
 
 pubring-stamp: $(top_srcdir)/tests/gpg/pubdemo.asc \
 	             $(top_srcdir)/tests/gpg/secdemo.asc
 	-$(TESTS_ENVIRONMENT) gpgconf --kill all
 	echo "ignore-invalid-option allow-loopback-pinentry" > $(abs_builddir)/gpg-agent.conf
 	echo "allow-loopback-pinentry" >> gpg-agent.conf
 	echo "ignore-invalid-option pinentry-mode" > gpg.conf
 	echo "pinentry-mode loopback" >> gpg.conf
 	$(TESTS_ENVIRONMENT) $(GPG) --no-permission-warning \
            --import $(top_srcdir)/tests/gpg/pubdemo.asc
 	$(TESTS_ENVIRONMENT) $(GPG) --no-permission-warning \
 		   --passphrase "abc" \
            --import $(top_srcdir)/tests/gpg/secdemo.asc
 	-$(TESTS_ENVIRONMENT) gpgconf --kill all
 	touch pubring-stamp
 
 .cpp.moc:
 	$(MOC) `test -f '$<' || echo '$(srcdir)/'`$< -o $@
 
 .h.hmoc:
 	$(MOC) `test -f '$<' || echo '$(srcdir)/'`$< -o $@
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 <dev@ingo-kloecker.de>
+
+    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 <protocol.h>
+#include <verifyopaquejob.h>
+
+#include <QCommandLineParser>
+#include <QCoreApplication>
+#include <QDebug>
+#include <QFile>
+#include <QTimer>
+
+#include <context.h>
+#include <verificationresult.h>
+
+#include <iostream>
+
+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<QFile> 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<QFile> 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();
+}