[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 &lt;[email protected]&gt; (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 &lt;[email protected]&gt; (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 &lt;[email protected]&gt; (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 &lt;[email protected]&gt; (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 &lt;[email protected]&gt; (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 &lt;[email protected]&gt; (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 &lt;[email protected]&gt; (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 = {});
 }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.