[pim/libkleo] /: Extract code that assesses signature status and further information
Ingo Klöcker <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit d23c2b0c165acfcb62d214b3321662d1c5d4fba8 by Ingo Klöcker, on behalf of Ingo Klöcker.
Committed on 18/08/2026 at 09:29.
Pushed by kloecker into branch 'master'.
Extract code that assesses signature status and further information
This decouples the actual formatting of a signature from the
assessment and allows using it for other things (like showing additional
hints if the signature isn't all good).
GnuPG-bug-id: 8336
M +21 -11 autotests/formattingtest.cpp
M +1 -0 src/CMakeLists.txt
M +75 -124 src/utils/formatting.cpp
M +6 -0 src/utils/formatting.h
A +73 -0 src/utils/verification.cpp [License: GPL(v2.0+)]
M +45 -0 src/utils/verification.h
https://invent.kde.org/pim/libkleo/-/commit/d23c2b0c165acfcb62d214b3321662d1c5d4fba8
diff --git a/autotests/formattingtest.cpp b/autotests/formattingtest.cpp
index 014c6a97..ef622f20 100644
--- a/autotests/formattingtest.cpp
+++ b/autotests/formattingtest.cpp
@@ -11,6 +11,7 @@
#include <Libkleo/Formatting>
#include <Libkleo/KeyCache>
#include <Libkleo/Test>
+#include <Libkleo/Verification>
#include <QGpgME/CryptoConfig>
#include <QGpgME/Debug>
@@ -438,45 +439,47 @@ private Q_SLOTS:
QTest::addColumn<Signature::Summary>("sigSummary");
QTest::addColumn<gpg_err_code_t>("sigStatus");
QTest::addColumn<Signature::Validity>("sigValidity");
+ QTest::addColumn<Kleo::SignatureStatus>("signatureStatus");
QTest::addColumn<QString>("expected");
QTest::newRow("all-good")
- << static_cast<Signature::Summary>(Signature::Summary::Green | Signature::Summary::Valid) << GPG_ERR_NO_ERROR << Signature::Validity::Full //
+ << static_cast<Signature::Summary>(Signature::Summary::Green | Signature::Summary::Valid) << GPG_ERR_NO_ERROR << Signature::Validity::Full
+ << SignatureStatus::ValidAndFullyTrusted
<< u"Signature verification was successful: Data and signature match and the certificate is valid and trusted.<br/>"
"Signed by <a href=\"key:27E12CEFBE2E11FAF985106BD24D35D21E3C740D\">Certified Key <[email protected]> (DATE)</a> on DATETIME."_s;
QTest::newRow("key-expired")
- << Signature::Summary::KeyExpired << GPG_ERR_KEY_EXPIRED << Signature::Validity::Unknown
+ << Signature::Summary::KeyExpired << GPG_ERR_KEY_EXPIRED << Signature::Validity::Unknown << SignatureStatus::ValidButKeyExpired
<< u"The data cannot be trusted. Reason: The signing certificate has expired.<br/>"
"Signed by <a href=\"key:972263BC1577E48958A2AF7A6CFC883EEE0918B1\">Expired Key <[email protected]> (DATE)</a> on DATETIME."_s;
QTest::newRow("key-not-certified")
- << Signature::Summary::None << GPG_ERR_NO_ERROR << Signature::Validity::Unknown
+ << Signature::Summary::None << GPG_ERR_NO_ERROR << Signature::Validity::Unknown << SignatureStatus::ValidButNotFullyTrusted
<< u"The data cannot be trusted. Reason: It cannot be verified whether the data originates from the stated source.<br/>"
"Signed by <a href=\"key:9152100939FC36332EC5954AD7ADC02ACDFA945A\">Not Certified <[email protected]> (DATE)</a> on DATETIME."_s;
QTest::newRow("key-revoked")
- << Signature::Summary::KeyRevoked << GPG_ERR_CERT_REVOKED << Signature::Validity::Unknown
+ << Signature::Summary::KeyRevoked << GPG_ERR_CERT_REVOKED << Signature::Validity::Unknown << SignatureStatus::ValidButKeyRevoked
<< u"The data cannot be trusted. Reason: The signing certificate has been revoked.<br/>"
"Signed by <a href=\"key:BA80E58FB5EC794D6396D47ADABA14732513A6D6\">Revoked Key <[email protected]> (DATE)</a> on DATETIME."_s;
QTest::newRow("key-unknown")
- << Signature::Summary::KeyMissing << GPG_ERR_NO_PUBKEY << Signature::Validity::Unknown
+ << Signature::Summary::KeyMissing << GPG_ERR_NO_PUBKEY << Signature::Validity::Unknown << SignatureStatus::KeyMissing
<< u"The data cannot be trusted. Reason: The signature cannot be verified because the corresponding certificate is not available. The signing "
"certificate’s fingerprint is <a href=\"certificate:C8C6053CA0018BCB1C0D3C1AF9F33E35E1C16A17\">"
"C8C6 053C A001 8BCB 1C0D 3C1A F9F3 3E35 E1C1 6A17</a>."_s;
#if GPGME_VERSION_NUMBER >= 0x020103
if (GpgME::engineInfo(GpgME::GpgEngine).engineVersion() >= "2.5.22") {
QTest::newRow("signature-bad")
- << Signature::Summary::Red << GPG_ERR_BAD_SIGNATURE << Signature::Validity::Unknown
+ << Signature::Summary::Red << GPG_ERR_BAD_SIGNATURE << Signature::Validity::Unknown << SignatureStatus::Invalid
<< u"The data cannot be trusted. Reason: Data and signature do not match.<br/>"
"The signature claims to be from <a href=\"key:117C22E18017CB18A67FC3D699954415471E4A5F\">Second UID <[email protected]> (DATE)</a> and is dated DATETIME."_s;
} else
#endif
{
QTest::newRow("signature-bad")
- << Signature::Summary::Red << GPG_ERR_BAD_SIGNATURE << Signature::Validity::Unknown
+ << Signature::Summary::Red << GPG_ERR_BAD_SIGNATURE << Signature::Validity::Unknown << SignatureStatus::Invalid
<< u"The data cannot be trusted. Reason: Data and signature do not match.<br/>"
"The signature claims to be from <a href=\"key:117C22E18017CB18A67FC3D699954415471E4A5F\">Second UID <[email protected]> (DATE)</a>."_s;
}
QTest::newRow("signature-expired")
- << Signature::Summary::SigExpired << GPG_ERR_SIG_EXPIRED << Signature::Validity::Unknown
+ << Signature::Summary::SigExpired << GPG_ERR_SIG_EXPIRED << Signature::Validity::Unknown << SignatureStatus::ValidButSignatureExpired
<< u"The data cannot be trusted. Reason: The signature has expired.<br/>"
"Signed by <a href=\"key:9152100939FC36332EC5954AD7ADC02ACDFA945A\">Not Certified <[email protected]> (DATE)</a> on DATETIME."_s;
}
@@ -486,6 +489,7 @@ private Q_SLOTS:
QFETCH(Signature::Summary, sigSummary);
QFETCH(gpg_err_code_t, sigStatus);
QFETCH(Signature::Validity, sigValidity);
+ QFETCH(Kleo::SignatureStatus, signatureStatus);
QFETCH(QString, expected);
const auto currentDataTag = QString::fromLatin1(QTest::currentDataTag());
@@ -510,7 +514,9 @@ private Q_SLOTS:
QCOMPARE(sig.status().code(), sigStatus);
QCOMPARE(sig.validity(), sigValidity);
- const QString result = Formatting::prettyDataSignature(verificationResult.signature(0), {});
+ const SignatureData sigData = Kleo::assessSignature(verificationResult.signature(0));
+ QCOMPARE(sigData.status, signatureStatus);
+ const QString result = Formatting::prettyDataSignature(sigData);
QCOMPARE(maskDateAndTime(result), expected);
}
@@ -529,7 +535,9 @@ private Q_SLOTS:
QVERIFY(!verificationResult.error());
QCOMPARE(verificationResult.numSignatures(), 1);
- const QString result = Formatting::prettyDataSignature(verificationResult.signature(0), {});
+ const SignatureData sigData = Kleo::assessSignature(verificationResult.signature(0));
+ QCOMPARE(sigData.status, SignatureStatus::KeyMissing);
+ const QString result = Formatting::prettyDataSignature(sigData);
const auto expected = u"The data cannot be trusted. Reason: The signature cannot be verified because the corresponding certificate is not available."_s
#if GPGMEPP_VERSION >= QT_VERSION_CHECK(2, 1, 1)
+ ((GpgME::engineInfo(GpgME::GpgEngine).engineVersion() >= "2.5.22")
@@ -610,7 +618,9 @@ private Q_SLOTS:
QCOMPARE(sig.validity(), sigValidity);
QCOMPARE(sig.isDeVs(), isDeVs);
- const QString result = Formatting::prettyDataSignature(verificationResult.signature(0), {});
+ const SignatureData sigData = Kleo::assessSignature(verificationResult.signature(0));
+ QCOMPARE(sigData.status, SignatureStatus::ValidAndFullyTrusted);
+ const QString result = Formatting::prettyDataSignature(sigData);
// replace COMPLIANCE with the actual compliance text which may or may not have a trailing "(beta)"
expected.replace(u"COMPLIANCE"_s, DeVSCompliance::name(true));
QCOMPARE(maskDateAndTime(result), expected);
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 7fc966de..37c20597 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -128,6 +128,7 @@ target_sources(KPim6Libkleo PRIVATE
utils/test.h
utils/validation.cpp
utils/validation.h
+ utils/verification.cpp
utils/verification.h
utils/multivalidator_p.h
utils/multivalidator.cpp
diff --git a/src/utils/formatting.cpp b/src/utils/formatting.cpp
index b211ce2f..5b1333a9 100644
--- a/src/utils/formatting.cpp
+++ b/src/utils/formatting.cpp
@@ -22,6 +22,7 @@
#include <libkleo/dnattributes.h>
#include <libkleo/keycache.h>
#include <libkleo/keygroup.h>
+#include <libkleo/verification.h>
#include <libkleo_debug.h>
@@ -1552,11 +1553,6 @@ static QString renderSMIMECertificateReference(const char *issuerSerial, const c
}
#endif
-static QDateTime signatureCreationTime(const GpgME::Signature &sig)
-{
- return sig.creationTime() != 0 ? QDateTime::fromSecsSinceEpoch(quint32(sig.creationTime())) : QDateTime();
-}
-
static QString renderSignatureCreationTime(const QDateTime &dt)
{
return dt.isValid() ? QLocale().toString(dt, QLocale::ShortFormat) : QString{};
@@ -1674,60 +1670,32 @@ QString Formatting::email(const GpgME::UserID &uid)
return {};
}
-static GpgME::UserID findUserIDByMailbox(const GpgME::Key &key, const QString &email)
-{
- const auto userIDs{key.userIDs()};
- for (const GpgME::UserID &id : userIDs) {
- if (!Formatting::email(id).compare(email, Qt::CaseInsensitive)) {
- return id;
- }
- }
- return {};
-}
-
QString Kleo::Formatting::prettySignature(const GpgME::Signature &sig, const QString &sender)
{
- if (sig.isNull()) {
- return QString();
- }
+ SignatureData sigData = assessSignature(sig, sender);
- const GpgME::Key key = Kleo::KeyCache::instance()->findSigner(sig);
+ const QString text = formatSigningInformation(sigData.sig, sigData.key, sigData.userID) + QLatin1StringView("<br/>");
- GpgME::UserID senderUid;
- if (!sender.isEmpty()) {
- senderUid = findUserIDByMailbox(key, sender);
- }
- if (senderUid.isNull() && !key.isNull()) {
- senderUid = key.userID(0);
- }
-
- const QString text = formatSigningInformation(sig, key, senderUid) + QLatin1StringView("<br/>");
-
- // Green
- if ((sig.summary() & GpgME::Signature::Valid)) {
- return text + formatValidSignatureWithTrustLevel(!senderUid.isNull() ? senderUid : key.userID(0));
- }
-
- // Red
- if ((sig.summary() & GpgME::Signature::Red)) {
- const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary()));
- if (sig.summary() & GpgME::Signature::SysError) {
- return ret + QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sig.status()));
+ switch (sigData.status) {
+ case SignatureStatus::NoSignature:
+ return {};
+ case SignatureStatus::ValidAndFullyTrusted:
+ return text + formatValidSignatureWithTrustLevel(sigData.userID);
+ case SignatureStatus::Invalid:
+ case SignatureStatus::ValidButSignerUntrustworthy: {
+ const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sigData.sig.summary()));
+ if (sigData.sig.summary() & GpgME::Signature::SysError) {
+ return ret + QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sigData.sig.status()));
}
return ret;
}
-
- // Key missing
- if ((sig.summary() & GpgME::Signature::KeyMissing)) {
+ case SignatureStatus::KeyMissing:
return text + i18n("You can search the certificate on a keyserver or import it from a file.");
- }
-
- // Good signature with some caveats
- if (sig.status().code() == GPG_ERR_NO_ERROR) {
- if ((sig.validity() == GpgME::Signature::Validity::Undefined) //
- || (sig.validity() == GpgME::Signature::Validity::Unknown)) {
+ case SignatureStatus::ValidButNotFullyTrusted: {
+ if ((sigData.sig.validity() == GpgME::Signature::Validity::Undefined) //
+ || (sigData.sig.validity() == GpgME::Signature::Validity::Unknown)) {
return text
- + (key.protocol() == GpgME::OpenPGP //
+ + (sigData.key.protocol() == GpgME::OpenPGP //
? i18nc("@info", "The signature is valid but the used key is not certified by you or any trusted person.")
: i18nc("@info",
"The signature is valid but the used certificate is not certified by a trustworthy Certificate "
@@ -1736,15 +1704,13 @@ QString Kleo::Formatting::prettySignature(const GpgME::Signature &sig, const QSt
+ i18nc("@info", "<strong>Warning:</strong> There is no indication that the signature belongs to the owner.");
}
// validity must be marginal; "never" results in bad signature, and "full" and "ultimate" result in Valid summary
- Q_ASSERT(sig.validity() == GpgME::Signature::Validity::Marginal);
+ Q_ASSERT(sigData.sig.validity() == GpgME::Signature::Validity::Marginal);
// marginal validity only occurs for OpenPGP
return text + i18nc("@info", "The signature is valid but the used key is not certified by you or any trusted person.") + "<br>"_L1
+ i18nc("@info", "<strong>Warning:</strong> It is not certain that the signature belongs to the owner.");
}
-
- // Expired signature (only occurs for OpenPGP)
- if (sig.status().code() == GPG_ERR_SIG_EXPIRED) {
- switch (sig.validity()) {
+ case SignatureStatus::ValidButSignatureExpired: {
+ switch (sigData.sig.validity()) {
case GpgME::Signature::Validity::Ultimate:
case GpgME::Signature::Validity::Full:
return text + i18nc("@info", "The signature is valid but it has expired.");
@@ -1760,128 +1726,113 @@ QString Kleo::Formatting::prettySignature(const GpgME::Signature &sig, const QSt
case GpgME::Signature::Validity::Never: // means bad signature
;
}
+ break;
}
-
- // Good signature but expired signing key
- if (sig.status().code() == GPG_ERR_KEY_EXPIRED) {
- return text + i18nc("@info", "The signature is valid but the used key has expired.");
+ case SignatureStatus::ValidButKeyExpired:
// don't print information about the key's validity; gpg also doesn't do this and since the key is expired it makes little sense
- }
-
- // Good signature but revoked signing key
- if (sig.status().code() == GPG_ERR_CERT_REVOKED) {
+ return text + i18nc("@info", "The signature is valid but the used key has expired.");
+ case SignatureStatus::ValidButKeyRevoked:
return text + i18nc("@info", "The signature is valid but the used key has been revoked.") + "<br>"_L1 //
+ i18nc("@info", "<strong>Warning:</strong> This could mean that the signature is forged.") + "<br>"_L1 //
- + (key.protocol() == GpgME::OpenPGP //
+ + (sigData.key.protocol() == GpgME::OpenPGP //
? i18nc("@info", "The used key is not certified by you or any trusted person.")
: i18nc("@info",
"The signature is valid but the used certificate is not certified by a trustworthy Certificate "
"Authority or the Certificate Authority is unknown."))
+ "<br>"_L1 //
+ i18nc("@info", "<strong>Warning:</strong> There is no indication that the signature belongs to the owner.");
+ case SignatureStatus::OtherError:
+ // fall through
+ ;
}
// Catch all fall through
- const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary()));
- if (sig.summary() & GpgME::Signature::SysError) {
- return ret + QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sig.status()));
+ QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sigData.sig.summary()));
+ if (sigData.sig.summary() & GpgME::Signature::SysError) {
+ ret += QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sigData.sig.status()));
}
return ret;
}
QString Kleo::Formatting::prettyDataSignature(const GpgME::Signature &sig, const QString &sender)
{
- if (sig.isNull()) {
- return QString();
- }
-
- const GpgME::Key key = Kleo::KeyCache::instance()->findSigner(sig);
-
- GpgME::UserID userID;
- if (!sender.isEmpty()) {
- userID = findUserIDByMailbox(key, sender);
- }
- if (userID.isNull() && !key.isNull()) {
- userID = key.userID(0);
- }
+ return prettyDataSignature(assessSignature(sig, sender));
+}
- // Valid (implies Green)
- if ((sig.summary() & GpgME::Signature::Valid)) {
+QString Kleo::Formatting::prettyDataSignature(const Kleo::SignatureData &sigData)
+{
+ switch (sigData.status) {
+ case SignatureStatus::NoSignature:
+ return {};
+ case SignatureStatus::ValidAndFullyTrusted:
return i18nc("@info", "Signature verification was successful: Data and signature match and the certificate is valid and trusted.") + "<br/>"_L1
- + renderSignedByOn(sig, key, userID) //
- + renderSignatureCompliance(sig);
- }
-
- // Red (means either bad signature or validity "never" for the signing key)
- if ((sig.summary() & GpgME::Signature::Red)) {
- const QDateTime sigCreationTime = signatureCreationTime(sig);
- const QString reason = (sig.status().code() == GPG_ERR_BAD_SIGNATURE) //
+ + renderSignedByOn(sigData.sig, sigData.key, sigData.userID) //
+ + renderSignatureCompliance(sigData.sig);
+ case SignatureStatus::Invalid:
+ case SignatureStatus::ValidButSignerUntrustworthy: {
+ const QString reason = (sigData.status == SignatureStatus::Invalid) //
? i18nc("@info", "Data and signature do not match.")
: i18nc("@info", "The signing certificate must not be trusted."); // happens with TOFU trust model and with failed S/MIME certificate chain auditing
QString text = i18nc("@info", "The data cannot be trusted. Reason: %1", reason) + "<br/>"_L1;
- if (sigCreationTime.isValid()) {
- text +=
- i18nc("@info", "The signature claims to be from %1 and is dated %2.", renderKeyV2(key, userID), renderSignatureCreationTime(sigCreationTime));
+ if (sigData.creationTime.isValid()) {
+ text += i18nc("@info",
+ "The signature claims to be from %1 and is dated %2.",
+ renderKeyV2(sigData.key, sigData.userID),
+ renderSignatureCreationTime(sigData.creationTime));
} else {
- text += i18nc("@info", "The signature claims to be from %1.", renderKeyV2(key, userID));
+ text += i18nc("@info", "The signature claims to be from %1.", renderKeyV2(sigData.key, sigData.userID));
}
return text;
}
-
- // Key missing
- if ((sig.summary() & GpgME::Signature::KeyMissing)) {
+ case SignatureStatus::KeyMissing: {
const QString reason = i18nc("@info", "The signature cannot be verified because the corresponding certificate is not available.");
QString text = i18nc("@info", "The data cannot be trusted. Reason: %1", reason);
// TODO: Print signing certificate’s issuer and S/N for missing S/MIME certificate once we get this data from gpgsm.
- if (sig.fingerprint()) {
+ if (sigData.sig.fingerprint()) {
text += u' ';
- text += i18nc("@info", "The signing certificate’s fingerprint is %1.", renderFingerprintLinkV2(sig.fingerprint()));
+ text += i18nc("@info", "The signing certificate’s fingerprint is %1.", renderFingerprintLinkV2(sigData.sig.fingerprint()));
#if GPGMEPP_VERSION >= QT_VERSION_CHECK(2, 1, 1)
- } else if (sig.issuerSerial() && sig.issuerName()) {
+ } else if (sigData.sig.issuerSerial() && sigData.sig.issuerName()) {
text += u' ';
text += i18nc("@info",
"The signing certificate’s serial number and issuer are %1.",
- renderSMIMECertificateReference(sig.issuerSerial(), sig.issuerName()));
+ renderSMIMECertificateReference(sigData.sig.issuerSerial(), sigData.sig.issuerName()));
#endif
}
return text;
}
-
- // Good signature with some caveats
- if (sig.status().isSuccess()) {
+ case SignatureStatus::ValidButNotFullyTrusted: {
const QString reason = i18nc("@info", "It cannot be verified whether the data originates from the stated source.");
return i18nc("@info", "The data cannot be trusted. Reason: %1", reason) + "<br/>"_L1 //
- + renderSignedByOn(sig, key, userID) //
- + renderSignatureCompliance(sig);
+ + renderSignedByOn(sigData.sig, sigData.key, sigData.userID) //
+ + renderSignatureCompliance(sigData.sig);
}
-
- // Expired signature (only occurs for OpenPGP)
- if (sig.status().code() == GPG_ERR_SIG_EXPIRED) {
+ case SignatureStatus::ValidButSignatureExpired: {
const QString reason = i18nc("@info", "The signature has expired.");
return i18nc("@info", "The data cannot be trusted. Reason: %1", reason) + "<br/>"_L1 //
- + renderSignedByOn(sig, key, userID) //
- + renderSignatureCompliance(sig);
+ + renderSignedByOn(sigData.sig, sigData.key, sigData.userID) //
+ + renderSignatureCompliance(sigData.sig);
}
-
- // Good signature but expired signing key
- if (sig.status().code() == GPG_ERR_KEY_EXPIRED) {
+ case SignatureStatus::ValidButKeyExpired: {
const QString reason = i18nc("@info", "The signing certificate has expired.");
return i18nc("@info", "The data cannot be trusted. Reason: %1", reason) + "<br/>"_L1 //
- + renderSignedByOn(sig, key, userID) //
- + renderSignatureCompliance(sig);
+ + renderSignedByOn(sigData.sig, sigData.key, sigData.userID) //
+ + renderSignatureCompliance(sigData.sig);
}
-
- // Good signature but revoked signing key
- if (sig.status().code() == GPG_ERR_CERT_REVOKED) {
+ case SignatureStatus::ValidButKeyRevoked: {
const QString reason = i18nc("@info", "The signing certificate has been revoked.");
return i18nc("@info", "The data cannot be trusted. Reason: %1", reason) + "<br/>"_L1 //
- + renderSignedByOn(sig, key, userID);
+ + renderSignedByOn(sigData.sig, sigData.key, sigData.userID);
+ }
+ case SignatureStatus::OtherError:
+ // fall through
+ ;
}
- // Catch all fall through
- QString text = i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary()));
- if (sig.summary() & GpgME::Signature::SysError) {
- text += "<br/>"_L1 + i18nc("@info", "Error: %1", Kleo::Formatting::errorAsString(sig.status()));
+ // Catch all
+ QString text = i18n("The signature is invalid: %1", signatureSummaryToString(sigData.sig.summary()));
+ if (sigData.sig.summary() & GpgME::Signature::SysError) {
+ text += "<br/>"_L1 + i18nc("@info", "Error: %1", Kleo::Formatting::errorAsString(sigData.sig.status()));
}
return text;
}
diff --git a/src/utils/formatting.h b/src/utils/formatting.h
index b19c256c..e5b67c7e 100644
--- a/src/utils/formatting.h
+++ b/src/utils/formatting.h
@@ -30,6 +30,7 @@ class Import;
namespace Kleo
{
class KeyGroup;
+struct SignatureData;
namespace Formatting
{
@@ -111,6 +112,11 @@ KLEO_EXPORT QString prettySignature(const GpgME::Signature &signature, const QSt
*/
KLEO_EXPORT QString prettyDataSignature(const GpgME::Signature &signature, const QString &sender);
+/*!
+ * This is an overload of prettyDataSignature that takes the result of Kleo::assessSignature().
+ */
+KLEO_EXPORT QString prettyDataSignature(const Kleo::SignatureData &signature);
+
// clang-format off
enum ToolTipOption {
KeyID = 0x001,
diff --git a/src/utils/verification.cpp b/src/utils/verification.cpp
new file mode 100644
index 00000000..b8eaa26b
--- /dev/null
+++ b/src/utils/verification.cpp
@@ -0,0 +1,73 @@
+/*
+ This file is part of libkleopatra
+ SPDX-FileCopyrightText: 2026 g10 Code GmbH
+ SPDX-FileContributor: Ingo Klöcker <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "verification.h"
+
+#include <libkleo/formatting.h>
+#include <libkleo/keycache.h>
+
+using namespace Kleo;
+
+static GpgME::UserID findUserIDByMailbox(const GpgME::Key &key, const QString &email)
+{
+ const auto userIDs{key.userIDs()};
+ for (const GpgME::UserID &id : userIDs) {
+ if (!Formatting::email(id).compare(email, Qt::CaseInsensitive)) {
+ return id;
+ }
+ }
+ return {};
+}
+
+QDateTime Kleo::signatureCreationTime(const GpgME::Signature &sig)
+{
+ return sig.creationTime() != 0 ? QDateTime::fromSecsSinceEpoch(quint32(sig.creationTime())) : QDateTime();
+}
+
+Kleo::SignatureData Kleo::assessSignature(const GpgME::Signature &sig, const QString &sender)
+{
+ SignatureData sigData;
+ sigData.sig = sig;
+
+ if (sig.isNull()) {
+ return sigData;
+ }
+
+ sigData.key = Kleo::KeyCache::instance()->findSigner(sig);
+
+ if (!sender.isEmpty()) {
+ sigData.userID = findUserIDByMailbox(sigData.key, sender);
+ }
+ if (sigData.userID.isNull() && !sigData.key.isNull()) {
+ sigData.userID = sigData.key.userID(0);
+ }
+
+ sigData.creationTime = signatureCreationTime(sig);
+
+ if ((sig.summary() & GpgME::Signature::Valid)) {
+ // Valid (implies Green)
+ sigData.status = SignatureStatus::ValidAndFullyTrusted;
+ } else if ((sig.summary() & GpgME::Signature::Red)) {
+ // Red means either bad signature or validity "never" for the signing key
+ sigData.status = (sig.status().code() == GPG_ERR_BAD_SIGNATURE) ? SignatureStatus::Invalid : SignatureStatus::ValidButSignerUntrustworthy;
+ } else if ((sig.summary() & GpgME::Signature::KeyMissing)) {
+ sigData.status = SignatureStatus::KeyMissing;
+ } else if (sig.status().isSuccess()) {
+ sigData.status = SignatureStatus::ValidButNotFullyTrusted;
+ } else if (sig.status().code() == GPG_ERR_SIG_EXPIRED) {
+ sigData.status = SignatureStatus::ValidButSignatureExpired;
+ } else if (sig.status().code() == GPG_ERR_KEY_EXPIRED) {
+ sigData.status = SignatureStatus::ValidButKeyExpired;
+ } else if (sig.status().code() == GPG_ERR_CERT_REVOKED) {
+ sigData.status = SignatureStatus::ValidButKeyRevoked;
+ } else {
+ sigData.status = SignatureStatus::OtherError;
+ }
+
+ return sigData;
+}
diff --git a/src/utils/verification.h b/src/utils/verification.h
index 269b9e38..5f02f57e 100644
--- a/src/utils/verification.h
+++ b/src/utils/verification.h
@@ -8,10 +8,36 @@
#pragma once
+#include "kleo_export.h"
+
+#include <QDateTime>
+
+#include <gpgme++/key.h>
#include <gpgme++/verificationresult.h>
namespace Kleo
{
+enum class SignatureStatus {
+ NoSignature,
+ KeyMissing, // Signing key is missing
+ ValidAndFullyTrusted, // Good signature made with fully trusted key
+ ValidButNotFullyTrusted, // Good signature made with not fully trusted key
+ ValidButSignatureExpired, // Good signature but expired signature (only occurs for OpenPGP)
+ ValidButKeyExpired, // Good signature but expired signing key
+ ValidButKeyRevoked, // Good signature but revoked signing key
+ ValidButSignerUntrustworthy, // Good signature with key flagged as untrustworthy (== must not be trusted)
+ Invalid, // Signature does not match the data
+ OtherError, // Another error occurred while checking the signature
+};
+
+struct SignatureData {
+ SignatureStatus status = SignatureStatus::NoSignature;
+ GpgME::Signature sig;
+ GpgME::Key key;
+ GpgME::UserID userID;
+ QDateTime creationTime;
+};
+
/*!
* Returns true if the signature \a sig is a good signature, i.e. it matches the signed data.
* It does not take into account the validity of the signing key, i.e. use it with care.
@@ -40,4 +66,23 @@ inline bool signatureIsBad(const GpgME::Signature &sig)
{
return (sig.status().code() == GPG_ERR_BAD_SIGNATURE);
}
+
+/*!
+ * Returns the creation time of the signature \a sig or an invalid QDateTime if no creation time is
+ * available (i.e. if sig.creationTime() is 0).
+ */
+KLEO_EXPORT QDateTime signatureCreationTime(const GpgME::Signature &sig);
+
+/*!
+ * Assesses a signature \a sig.
+ *
+ * Looks at the properties of the signature \a sig and makes an assessment about the status of the signature.
+ * Additionally, it returns the key that was used for creating the signature and the creation time of the
+ * signature (if available). If \a sender is given then it looks for a matching user ID. Otherwise, the primary
+ * user ID of the key is returned.
+ *
+ * @param signature The signature to assess.
+ * @param sender The sender of the signature (e.g. in case of email).
+ */
+KLEO_EXPORT SignatureData assessSignature(const GpgME::Signature &sig, const QString &sender = {});
}