File indexing completed on 2024-05-19 05:22:50

0001 /*
0002     This file is part of libkleopatra's test suite.
0003     SPDX-FileCopyrightText: 2022 Sandro Knauß <knauss@kde.org>
0004     SPDX-FileCopyrightText: 2023 g10 Code GmbH
0005     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "testhelpers.h"
0011 
0012 #include <Libkleo/Chrono>
0013 #include <Libkleo/ExpiryChecker>
0014 #include <Libkleo/ExpiryCheckerSettings>
0015 #include <Libkleo/Formatting>
0016 #include <Libkleo/KeyCache>
0017 
0018 #include <QDebug>
0019 #include <QProcess>
0020 #include <QRegularExpression>
0021 #include <QSignalSpy>
0022 #include <QTemporaryDir>
0023 #include <QTest>
0024 
0025 using namespace Kleo;
0026 using namespace GpgME;
0027 
0028 Q_DECLARE_METATYPE(GpgME::Key)
0029 
0030 using days = Kleo::chrono::days;
0031 
0032 class FakeTimeProvider : public Kleo::TimeProvider
0033 {
0034 public:
0035     explicit FakeTimeProvider(const QDateTime &dateTime)
0036         : mCurrentDate{dateTime.date()}
0037         , mCurrentTime{dateTime.toSecsSinceEpoch()}
0038     {
0039     }
0040 
0041     time_t currentTime() const override
0042     {
0043         return mCurrentTime;
0044     }
0045 
0046     QDate currentDate() const override
0047     {
0048         return mCurrentDate;
0049     }
0050 
0051     Qt::TimeSpec timeSpec() const override
0052     {
0053         // use UTC to avoid test failures caused by "wrong" local timezone
0054         return Qt::UTC;
0055     }
0056 
0057 private:
0058     QDate mCurrentDate;
0059     time_t mCurrentTime;
0060 };
0061 
0062 class ExpiryCheckerTest : public QObject
0063 {
0064     Q_OBJECT
0065 
0066 private Q_SLOTS:
0067     void initTestCase()
0068     {
0069         qRegisterMetaType<ExpiryChecker::ExpiryInformation>();
0070 
0071         mGnupgHome = QTest::qExtractTestData(QStringLiteral("/fixtures/expirycheckertest"));
0072         qputenv("GNUPGHOME", mGnupgHome->path().toLocal8Bit());
0073 
0074         // hold a reference to the key cache to avoid rebuilding while the test is running
0075         mKeyCache = KeyCache::instance();
0076         // make sure that the key cache has been populated
0077         (void)mKeyCache->keys();
0078     }
0079 
0080     void cleanupTestCase()
0081     {
0082         // verify that nobody else holds a reference to the key cache
0083         QVERIFY(mKeyCache.use_count() == 1);
0084         mKeyCache.reset();
0085 
0086         (void)QProcess::execute(QStringLiteral("gpgconf"), {"--kill", "all"});
0087 
0088         mGnupgHome.reset();
0089         qunsetenv("GNUPGHOME");
0090     }
0091 
0092     void errorHandling_data()
0093     {
0094         QTest::addColumn<GpgME::Key>("key");
0095         QTest::addColumn<ExpiryChecker::CheckFlags>("checkFlags");
0096         QTest::addColumn<ExpiryChecker::ExpirationStatus>("expectedStatus");
0097 
0098         QTest::newRow("invalid key") //
0099             << GpgME::Key{} //
0100             << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} //
0101             << ExpiryChecker::InvalidKey;
0102         QTest::newRow("invalid flags - no flags") //
0103             << testKey("test@kolab.org", GpgME::OpenPGP) //
0104             << ExpiryChecker::CheckFlags{} //
0105             << ExpiryChecker::InvalidCheckFlags;
0106         QTest::newRow("invalid flags - no usage flags") //
0107             << testKey("test@kolab.org", GpgME::OpenPGP) //
0108             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnKey | ExpiryChecker::CheckChain} //
0109             << ExpiryChecker::InvalidCheckFlags;
0110     }
0111 
0112     void errorHandling()
0113     {
0114         QFETCH(GpgME::Key, key);
0115         QFETCH(ExpiryChecker::CheckFlags, checkFlags);
0116         QFETCH(ExpiryChecker::ExpirationStatus, expectedStatus);
0117 
0118         ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{1}, days{1}, days{1}});
0119         QTest::ignoreMessage(QtWarningMsg, QRegularExpression{QStringLiteral("checkKey called with")});
0120         const auto result = checker.checkKey(key, checkFlags);
0121         QCOMPARE(result.expiration.certificate, key);
0122         QCOMPARE(result.expiration.status, expectedStatus);
0123     }
0124 
0125     void valid_data()
0126     {
0127         QTest::addColumn<GpgME::Key>("key");
0128         QTest::addColumn<QDateTime>("fakedate");
0129         // use dates between creation date and expiration date (if there is one) of the test keys/certificates
0130         QTest::newRow("neverExpire") << testKey("test@kolab.org", GpgME::OpenPGP) << QDateTime{{2012, 1, 1}, {}, QTimeZone::UTC};
0131         QTest::newRow("openpgp") << testKey("alice@autocrypt.example", GpgME::OpenPGP) << QDateTime{{2020, 1, 1}, {}, QTimeZone::UTC};
0132         QTest::newRow("smime") << testKey("test@example.com", GpgME::CMS) << QDateTime{{2012, 1, 1}, {}, QTimeZone::UTC};
0133     }
0134 
0135     void valid()
0136     {
0137         QFETCH(GpgME::Key, key);
0138         QFETCH(QDateTime, fakedate);
0139 
0140         ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{1}, days{1}, days{1}});
0141         checker.setTimeProviderForTest(std::make_shared<FakeTimeProvider>(fakedate));
0142         QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage);
0143 
0144         const auto result = checker.checkKey(key, ExpiryChecker::EncryptionKey);
0145         QCOMPARE(result.checkFlags, ExpiryChecker::EncryptionKey);
0146         QCOMPARE(result.expiration.certificate, key);
0147         QCOMPARE(result.expiration.status, ExpiryChecker::NotNearExpiry);
0148         QCOMPARE(spy.count(), 0);
0149     }
0150 
0151     void expired_data()
0152     {
0153         QTest::addColumn<GpgME::Key>("key");
0154         QTest::addColumn<ExpiryChecker::CheckFlags>("checkFlags");
0155         QTest::addColumn<QDateTime>("fakedate");
0156         QTest::addColumn<Kleo::chrono::days>("expectedDuration");
0157         QTest::addColumn<ExpiryChecker::ExpiryInformation>("expiryInfo");
0158         QTest::addColumn<QString>("msg");
0159 
0160         QTest::newRow("openpgp - other; 0 days ago") //
0161             << testKey("alice@autocrypt.example", GpgME::OpenPGP) //
0162             << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} //
0163             << QDateTime{{2021, 1, 21}, {23, 59, 59}, QTimeZone::UTC} // the last second of the day the key expired
0164             << days{0} //
0165             << ExpiryChecker::OtherKeyExpired
0166             << QStringLiteral(
0167                    "<p>The OpenPGP key for</p><p align=center><b>alice@autocrypt.example</b> (KeyID 0xF231550C4F47E38E)</p><p>expired less than a day "
0168                    "ago.</p>");
0169         QTest::newRow("openpgp - own; 1 day ago") //
0170             << testKey("alice@autocrypt.example", GpgME::OpenPGP) //
0171             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} //
0172             << QDateTime{{2021, 1, 22}, {}, QTimeZone::UTC} // the day after the expiration date of the key
0173             << days{1} //
0174             << ExpiryChecker::OwnKeyExpired
0175             << QStringLiteral(
0176                    "<p>Your OpenPGP encryption key</p><p align=center><b>alice@autocrypt.example</b> (KeyID 0xF231550C4F47E38E)</p><p>expired yesterday.</p>");
0177         QTest::newRow("openpgp - own signing; 2 days ago") //
0178             << testKey("alice@autocrypt.example", GpgME::OpenPGP) //
0179             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} //
0180             << QDateTime{{2021, 1, 23}, {}, QTimeZone::UTC} // the second day after the expiration date of the key
0181             << days{2} //
0182             << ExpiryChecker::OwnKeyExpired
0183             << QStringLiteral(
0184                    "<p>Your OpenPGP signing key</p><p align=center><b>alice@autocrypt.example</b> (KeyID 0xF231550C4F47E38E)</p><p>expired 2 days "
0185                    "ago.</p>");
0186 
0187         QTest::newRow("smime - other; 0 days ago") //
0188             << testKey("test@example.com", GpgME::CMS) //
0189             << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} //
0190             << QDateTime{{2013, 3, 25}, {23, 59, 59}, QTimeZone::UTC} // the last second of the day the key expired
0191             << days{0} //
0192             << ExpiryChecker::OtherKeyExpired
0193             << QStringLiteral(
0194                    "<p>The S/MIME certificate for</p><p align=center><b>CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US</b> (serial "
0195                    "number 00D345203A186385C9)</p><p>expired less than a day ago.</p>");
0196         QTest::newRow("smime - own; 1 day ago") //
0197             << testKey("test@example.com", GpgME::CMS) //
0198             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} //
0199             << QDateTime{{2013, 3, 26}, {}, QTimeZone::UTC} // the day after the expiration date of the key
0200             << days{1} //
0201             << ExpiryChecker::OwnKeyExpired
0202             << QStringLiteral(
0203                    "<p>Your S/MIME encryption certificate</p><p align=center><b>CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US</b> "
0204                    "(serial number 00D345203A186385C9)</p><p>expired yesterday.</p>");
0205         QTest::newRow("smime - own signing; 2 days ago") //
0206             << testKey("test@example.com", GpgME::CMS) //
0207             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} //
0208             << QDateTime{{2013, 3, 27}, {}, QTimeZone::UTC} // the second day after the expiration date of the key
0209             << days{2} //
0210             << ExpiryChecker::OwnKeyExpired
0211             << QStringLiteral(
0212                    "<p>Your S/MIME signing certificate</p><p align=center><b>CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US</b> "
0213                    "(serial number 00D345203A186385C9)</p><p>expired 2 days ago.</p>");
0214     }
0215 
0216     void expired()
0217     {
0218         QFETCH(GpgME::Key, key);
0219         QFETCH(ExpiryChecker::CheckFlags, checkFlags);
0220         QFETCH(QDateTime, fakedate);
0221         QFETCH(Kleo::chrono::days, expectedDuration);
0222         QFETCH(ExpiryChecker::ExpiryInformation, expiryInfo);
0223         QFETCH(QString, msg);
0224 
0225         {
0226             ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{1}, days{1}, days{1}});
0227             checker.setTimeProviderForTest(std::make_shared<FakeTimeProvider>(fakedate));
0228             QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage);
0229             const auto result = checker.checkKey(key, checkFlags);
0230             QCOMPARE(result.checkFlags, checkFlags);
0231             QCOMPARE(result.expiration.certificate, key);
0232             QCOMPARE(result.expiration.status, ExpiryChecker::Expired);
0233             QCOMPARE(result.expiration.duration, expectedDuration);
0234             QCOMPARE(spy.count(), 1);
0235             QList<QVariant> arguments = spy.takeFirst();
0236             QCOMPARE(arguments.at(0).value<GpgME::Key>().keyID(), key.keyID());
0237             QCOMPARE(arguments.at(1).toString(), msg);
0238             QCOMPARE(arguments.at(2).value<ExpiryChecker::ExpiryInformation>(), expiryInfo);
0239         }
0240     }
0241 
0242     void nearexpiry_data()
0243     {
0244         QTest::addColumn<GpgME::Key>("key");
0245         QTest::addColumn<QDateTime>("fakedate");
0246         QTest::addColumn<Kleo::chrono::days>("expectedDuration");
0247         QTest::addColumn<QString>("msg");
0248         QTest::addColumn<QString>("msgOwnKey");
0249         QTest::addColumn<QString>("msgOwnSigningKey");
0250 
0251         // use the day 5 days before the expiration date of the test keys/certificates as fake date
0252         QTest::newRow("openpgp")
0253             << testKey("alice@autocrypt.example", GpgME::OpenPGP) //
0254             << QDateTime{{2021, 1, 16}, {}, QTimeZone::UTC} //
0255             << days{5}
0256             << QStringLiteral(
0257                    "<p>The OpenPGP key for</p><p align=center><b>alice@autocrypt.example</b> (KeyID 0xF231550C4F47E38E)</p><p>expires in 5 days.</p>")
0258             << QStringLiteral(
0259                    "<p>Your OpenPGP encryption key</p><p align=center><b>alice@autocrypt.example</b> (KeyID 0xF231550C4F47E38E)</p><p>expires in 5 "
0260                    "days.</p>")
0261             << QStringLiteral(
0262                    "<p>Your OpenPGP signing key</p><p align=center><b>alice@autocrypt.example</b> (KeyID 0xF231550C4F47E38E)</p><p>expires in 5 "
0263                    "days.</p>");
0264         QTest::newRow("smime") << testKey("test@example.com", GpgME::CMS) //
0265                                << QDateTime{{2013, 3, 20}, {}, QTimeZone::UTC} //
0266                                << days{5}
0267                                << QStringLiteral(
0268                                       "<p>The S/MIME certificate for</p><p align=center><b>CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US</b> (serial "
0269                                       "number 00D345203A186385C9)</p><p>expires in 5 days.</p>")
0270                                << QStringLiteral(
0271                                       "<p>Your S/MIME encryption certificate</p><p align=center><b>CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US</b> "
0272                                       "(serial number 00D345203A186385C9)</p><p>expires in 5 days.</p>")
0273                                << QStringLiteral(
0274                                       "<p>Your S/MIME signing certificate</p><p align=center><b>CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US</b> "
0275                                       "(serial number 00D345203A186385C9)</p><p>expires in 5 days.</p>");
0276     }
0277 
0278     void nearexpiry()
0279     {
0280         QFETCH(GpgME::Key, key);
0281         QFETCH(QDateTime, fakedate);
0282         QFETCH(Kleo::chrono::days, expectedDuration);
0283         QFETCH(QString, msg);
0284         QFETCH(QString, msgOwnKey);
0285         QFETCH(QString, msgOwnSigningKey);
0286 
0287         {
0288             ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{10}, days{1}, days{1}});
0289             checker.setTimeProviderForTest(std::make_shared<FakeTimeProvider>(fakedate));
0290             QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage);
0291             // Test if the correct threshold is taken
0292             {
0293                 const auto result = checker.checkKey(key, ExpiryChecker::EncryptionKey);
0294                 QCOMPARE(result.checkFlags, ExpiryChecker::EncryptionKey);
0295                 QCOMPARE(result.expiration.certificate, key);
0296                 QCOMPARE(result.expiration.status, ExpiryChecker::ExpiresSoon);
0297                 QCOMPARE(result.expiration.duration, expectedDuration);
0298                 QCOMPARE(spy.count(), 1);
0299             }
0300             {
0301                 const auto result = checker.checkKey(key, ExpiryChecker::OwnEncryptionKey);
0302                 QCOMPARE(result.checkFlags, ExpiryChecker::OwnEncryptionKey);
0303                 QCOMPARE(result.expiration.certificate, key);
0304                 QCOMPARE(result.expiration.status, ExpiryChecker::NotNearExpiry);
0305                 QCOMPARE(result.expiration.duration, expectedDuration);
0306                 QCOMPARE(spy.count(), 1);
0307             }
0308             {
0309                 const auto result = checker.checkKey(key, ExpiryChecker::OwnSigningKey);
0310                 QCOMPARE(result.checkFlags, ExpiryChecker::OwnSigningKey);
0311                 QCOMPARE(result.expiration.certificate, key);
0312                 QCOMPARE(result.expiration.status, ExpiryChecker::NotNearExpiry);
0313                 QCOMPARE(result.expiration.duration, expectedDuration);
0314                 QCOMPARE(spy.count(), 1);
0315             }
0316             QList<QVariant> arguments = spy.takeFirst();
0317             QCOMPARE(arguments.at(0).value<GpgME::Key>().keyID(), key.keyID());
0318             QCOMPARE(arguments.at(1).toString(), msg);
0319             QCOMPARE(arguments.at(2).value<ExpiryChecker::ExpiryInformation>(), ExpiryChecker::OtherKeyNearExpiry);
0320         }
0321         {
0322             ExpiryChecker checker(ExpiryCheckerSettings{days{10}, days{1}, days{1}, days{1}});
0323             checker.setTimeProviderForTest(std::make_shared<FakeTimeProvider>(fakedate));
0324             QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage);
0325             // Test if the correct treshold is taken
0326             checker.checkKey(key, ExpiryChecker::EncryptionKey);
0327             checker.checkKey(key, ExpiryChecker::OwnEncryptionKey);
0328             QCOMPARE(spy.count(), 1);
0329             QList<QVariant> arguments = spy.takeFirst();
0330             QCOMPARE(arguments.at(0).value<GpgME::Key>().keyID(), key.keyID());
0331             QCOMPARE(arguments.at(1).toString(), msgOwnKey);
0332             QCOMPARE(arguments.at(2).value<ExpiryChecker::ExpiryInformation>(), ExpiryChecker::OwnKeyNearExpiry);
0333         }
0334         {
0335             ExpiryChecker checker(ExpiryCheckerSettings{days{10}, days{1}, days{1}, days{1}});
0336             checker.setTimeProviderForTest(std::make_shared<FakeTimeProvider>(fakedate));
0337             QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage);
0338             // Test if the correct treshold is taken
0339             checker.checkKey(key, ExpiryChecker::EncryptionKey);
0340             checker.checkKey(key, ExpiryChecker::OwnSigningKey);
0341             QCOMPARE(spy.count(), 1);
0342             QList<QVariant> arguments = spy.takeFirst();
0343             QCOMPARE(arguments.at(0).value<GpgME::Key>().keyID(), key.keyID());
0344             QCOMPARE(arguments.at(1).toString(), msgOwnSigningKey);
0345             QCOMPARE(arguments.at(2).value<ExpiryChecker::ExpiryInformation>(), ExpiryChecker::OwnKeyNearExpiry);
0346         }
0347     }
0348 
0349     void expiringEncryptionSubkey_data()
0350     {
0351         QTest::addColumn<GpgME::Key>("key");
0352         QTest::addColumn<ExpiryChecker::CheckFlags>("checkFlags");
0353         QTest::addColumn<QDateTime>("fakedate");
0354         QTest::addColumn<ExpiryChecker::ExpirationStatus>("expectedStatus");
0355         QTest::addColumn<Kleo::chrono::days>("expectedDuration");
0356 
0357         QTest::newRow("valid - sign") //
0358             << testKey("encr-expires@example.net", GpgME::OpenPGP) //
0359             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} //
0360             << QDateTime{{2023, 4, 18}, {}, QTimeZone::UTC} // 9 days before expiration of encryption subkey
0361             << ExpiryChecker::NotNearExpiry //
0362             << days{0}; // ignored
0363         QTest::newRow("valid - encrypt to self") //
0364             << testKey("encr-expires@example.net", GpgME::OpenPGP) //
0365             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} //
0366             << QDateTime{{2023, 4, 18}, {}, QTimeZone::UTC} // 9 days before expiration of encryption subkey
0367             << ExpiryChecker::NotNearExpiry //
0368             << days{0}; // ignored
0369         QTest::newRow("valid - encrypt to others") //
0370             << testKey("encr-expires@example.net", GpgME::OpenPGP) //
0371             << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} //
0372             << QDateTime{{2023, 4, 18}, {}, QTimeZone::UTC} // 9 days before expiration of encryption subkey
0373             << ExpiryChecker::NotNearExpiry //
0374             << days{0}; // ignored
0375         QTest::newRow("near expiry - sign") //
0376             << testKey("encr-expires@example.net", GpgME::OpenPGP) //
0377             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} //
0378             << QDateTime{{2023, 4, 26}, {}, QTimeZone::UTC} // 1 day before expiration of encryption subkey
0379             << ExpiryChecker::NotNearExpiry // signing key doesn't expire
0380             << days{0}; // ignored
0381         QTest::newRow("near expiry - encrypt to self") //
0382             << testKey("encr-expires@example.net", GpgME::OpenPGP) //
0383             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} //
0384             << QDateTime{{2023, 4, 26}, {}, QTimeZone::UTC} // 1 day before expiration of encryption subkey
0385             << ExpiryChecker::ExpiresSoon //
0386             << days{1};
0387         QTest::newRow("near expiry - encrypt to others") //
0388             << testKey("encr-expires@example.net", GpgME::OpenPGP) //
0389             << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} //
0390             << QDateTime{{2023, 4, 26}, {}, QTimeZone::UTC} // 1 day before expiration of encryption subkey
0391             << ExpiryChecker::ExpiresSoon //
0392             << days{1};
0393         QTest::newRow("expired - sign") //
0394             << testKey("encr-expires@example.net", GpgME::OpenPGP) //
0395             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} //
0396             << QDateTime{{2023, 4, 28}, {}, QTimeZone::UTC} // 1 day after expiration of encryption subkey
0397             << ExpiryChecker::NotNearExpiry // signing key doesn't expire
0398             << days{0}; // ignored
0399         QTest::newRow("expired - encrypt to self") //
0400             << testKey("encr-expires@example.net", GpgME::OpenPGP) //
0401             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} //
0402             << QDateTime{{2023, 4, 28}, {}, QTimeZone::UTC} // 1 day after expiration of encryption subkey
0403             << ExpiryChecker::Expired //
0404             << days{1};
0405         QTest::newRow("expired - encrypt to others") //
0406             << testKey("encr-expires@example.net", GpgME::OpenPGP) //
0407             << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} //
0408             << QDateTime{{2023, 4, 28}, {}, QTimeZone::UTC} // 1 day after expiration of encryption subkey
0409             << ExpiryChecker::Expired //
0410             << days{1};
0411     }
0412 
0413     void expiringEncryptionSubkey()
0414     {
0415         QFETCH(GpgME::Key, key);
0416         QFETCH(ExpiryChecker::CheckFlags, checkFlags);
0417         QFETCH(QDateTime, fakedate);
0418         QFETCH(ExpiryChecker::ExpirationStatus, expectedStatus);
0419         QFETCH(Kleo::chrono::days, expectedDuration);
0420 
0421         ExpiryChecker checker(ExpiryCheckerSettings{days{5}, days{5}, days{5}, days{5}});
0422         checker.setTimeProviderForTest(std::make_shared<FakeTimeProvider>(fakedate));
0423         const auto result = checker.checkKey(key, checkFlags);
0424         QCOMPARE(result.checkFlags, checkFlags);
0425         QCOMPARE(result.expiration.certificate, key);
0426         QCOMPARE(result.expiration.status, expectedStatus);
0427         if (expectedStatus != ExpiryChecker::NotNearExpiry) {
0428             // duration is undefined if status is NotNearExpiry
0429             QCOMPARE(result.expiration.duration, expectedDuration);
0430         }
0431     }
0432 
0433     void notExpiringEncryptionSubkey_data()
0434     {
0435         QTest::addColumn<GpgME::Key>("key");
0436         QTest::addColumn<ExpiryChecker::CheckFlags>("checkFlags");
0437         QTest::addColumn<QDateTime>("fakedate");
0438         QTest::addColumn<ExpiryChecker::ExpirationStatus>("expectedStatus");
0439         QTest::addColumn<Kleo::chrono::days>("expectedDuration");
0440 
0441         QTest::newRow("valid - sign") //
0442             << testKey("expires@example.net", GpgME::OpenPGP) //
0443             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} //
0444             << QDateTime{{2023, 4, 24}, {}, QTimeZone::UTC} // 9 days before expiration of primary key
0445             << ExpiryChecker::NotNearExpiry //
0446             << days{0}; // ignored
0447         QTest::newRow("valid - encrypt to self") //
0448             << testKey("expires@example.net", GpgME::OpenPGP) //
0449             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} //
0450             << QDateTime{{2023, 4, 24}, {}, QTimeZone::UTC} // 9 days before expiration of primary key
0451             << ExpiryChecker::NotNearExpiry //
0452             << days{0}; // ignored
0453         QTest::newRow("valid - encrypt to others") //
0454             << testKey("expires@example.net", GpgME::OpenPGP) //
0455             << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} //
0456             << QDateTime{{2023, 4, 24}, {}, QTimeZone::UTC} // 9 days before expiration of primary key
0457             << ExpiryChecker::NotNearExpiry //
0458             << days{0}; // ignored
0459         QTest::newRow("near expiry - sign") //
0460             << testKey("expires@example.net", GpgME::OpenPGP) //
0461             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} //
0462             << QDateTime{{2023, 5, 2}, {}, QTimeZone::UTC} // 1 day before expiration of primary key
0463             << ExpiryChecker::ExpiresSoon //
0464             << days{1};
0465         QTest::newRow("near expiry - encrypt to self") //
0466             << testKey("expires@example.net", GpgME::OpenPGP) //
0467             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} //
0468             << QDateTime{{2023, 5, 2}, {}, QTimeZone::UTC} // 1 day before expiration of primary key
0469             << ExpiryChecker::ExpiresSoon //
0470             << days{1};
0471         QTest::newRow("near expiry - encrypt to others") //
0472             << testKey("expires@example.net", GpgME::OpenPGP) //
0473             << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} //
0474             << QDateTime{{2023, 5, 2}, {}, QTimeZone::UTC} // 1 day before expiration of primary key
0475             << ExpiryChecker::ExpiresSoon //
0476             << days{1};
0477         QTest::newRow("expired - sign") //
0478             << testKey("expires@example.net", GpgME::OpenPGP) //
0479             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} //
0480             << QDateTime{{2023, 5, 4}, {}, QTimeZone::UTC} // 1 day after expiration of primary key
0481             << ExpiryChecker::Expired //
0482             << days{1};
0483         QTest::newRow("expired - encrypt to self") //
0484             << testKey("expires@example.net", GpgME::OpenPGP) //
0485             << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} //
0486             << QDateTime{{2023, 5, 4}, {}, QTimeZone::UTC} // 1 day after expiration of primary key
0487             << ExpiryChecker::Expired //
0488             << days{1};
0489         QTest::newRow("expired - encrypt to others") //
0490             << testKey("expires@example.net", GpgME::OpenPGP) //
0491             << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} //
0492             << QDateTime{{2023, 5, 4}, {}, QTimeZone::UTC} // 1 day after expiration of primary key
0493             << ExpiryChecker::Expired //
0494             << days{1};
0495     }
0496 
0497     void notExpiringEncryptionSubkey()
0498     {
0499         QFETCH(GpgME::Key, key);
0500         QFETCH(ExpiryChecker::CheckFlags, checkFlags);
0501         QFETCH(QDateTime, fakedate);
0502         QFETCH(ExpiryChecker::ExpirationStatus, expectedStatus);
0503         QFETCH(Kleo::chrono::days, expectedDuration);
0504 
0505         ExpiryChecker checker(ExpiryCheckerSettings{days{5}, days{5}, days{5}, days{5}});
0506         checker.setTimeProviderForTest(std::make_shared<FakeTimeProvider>(fakedate));
0507         const auto result = checker.checkKey(key, checkFlags);
0508         QCOMPARE(result.checkFlags, checkFlags);
0509         QCOMPARE(result.expiration.certificate, key);
0510         QCOMPARE(result.expiration.status, expectedStatus);
0511         if (expectedStatus != ExpiryChecker::NotNearExpiry) {
0512             // duration is undefined if status is NotNearExpiry
0513             QCOMPARE(result.expiration.duration, expectedDuration);
0514         }
0515     }
0516 
0517     void certificateChain_data()
0518     {
0519         QTest::addColumn<GpgME::Key>("key");
0520         QTest::addColumn<ExpiryChecker::CheckFlags>("checkFlags");
0521         QTest::addColumn<QDateTime>("fakedate");
0522         QTest::addColumn<ExpiryChecker::ExpirationStatus>("expectedStatus");
0523         QTest::addColumn<Kleo::chrono::days>("expectedDuration");
0524         QTest::addColumn<int>("expectedChainResults");
0525         QTest::addColumn<GpgME::Key>("expectedChainCertificate");
0526         QTest::addColumn<ExpiryChecker::ExpirationStatus>("expectedChainStatus");
0527         QTest::addColumn<Kleo::chrono::days>("expectedChainDuration");
0528         QTest::addColumn<int>("emissions");
0529         QTest::addColumn<QByteArray>("keyID");
0530         QTest::addColumn<QString>("msg");
0531 
0532         QTest::newRow("certificate near expiry; issuer okay") //
0533             << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) //
0534             << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey | ExpiryChecker::CheckChain} //
0535             << QDateTime{{2019, 6, 19}, {}, QTimeZone::UTC} // 5 days before expiration date of the certificate
0536             << ExpiryChecker::ExpiresSoon //
0537             << days{5} //
0538             << 0 // no expired or expiring certificates in issuer chain
0539             << Key{} // ignored
0540             << ExpiryChecker::ExpirationStatus{} // ignored
0541             << days{} // ignored
0542             << 1 // expect 1 signal emission because of a 2-certificate chain with 1 cert near expiry
0543             << QByteArray{"501F4DE8BE231B05"} // first signal emission references the certificate
0544             << QStringLiteral(
0545                    "<p>The S/MIME certificate for</p><p align=center><b>CN=AddTrust External CA Root,OU=AddTrust External TTP Network,O=AddTrust AB,C=SE</b> "
0546                    "(serial number 51260A931CE27F9CC3A55F79E072AE82)</p><p>expires in 5 days.</p>");
0547         QTest::newRow("certificate near expiry; issuer not checked") //
0548             << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) //
0549             << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey} //
0550             << QDateTime{{2019, 6, 19}, {}, QTimeZone::UTC} // 5 days before expiration date of the certificate
0551             << ExpiryChecker::ExpiresSoon //
0552             << days{5} //
0553             << 0 // issuer chain not checked
0554             << Key{} // ignored
0555             << ExpiryChecker::ExpirationStatus{} // ignored
0556             << days{} // ignored
0557             << 1 // expect 1 signal emission because certificate is near expiry
0558             << QByteArray{"501F4DE8BE231B05"} // signal emission references the certificate
0559             << QStringLiteral(
0560                    "<p>The S/MIME certificate for</p><p align=center><b>CN=AddTrust External CA Root,OU=AddTrust External TTP Network,O=AddTrust AB,C=SE</b> "
0561                    "(serial number 51260A931CE27F9CC3A55F79E072AE82)</p><p>expires in 5 days.</p>");
0562         QTest::newRow("certificate okay; issuer near expiry") //
0563             << testKey("9E99817D12280C9677674430492EDA1DCE2E4C63", GpgME::CMS) //
0564             << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey | ExpiryChecker::CheckChain} //
0565             << QDateTime{{2019, 6, 19}, {}, QTimeZone::UTC} // 5 days before expiration date of the issuer certificate
0566             << ExpiryChecker::NotNearExpiry //
0567             << days{346} //
0568             << 1 // one expiring certificate in issuer chain
0569             << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) //
0570             << ExpiryChecker::ExpiresSoon //
0571             << days{5} //
0572             << 1 // expect 1 signal emission because of a 2-certificate chain with 1 cert near expiry
0573             << QByteArray{"501F4DE8BE231B05"} // first signal emission references the isser certificate
0574             << QStringLiteral(
0575                    "<p>The intermediate CA certificate</p><p align=center><b>CN=AddTrust External CA Root,OU=AddTrust External TTP Network,O=AddTrust "
0576                    "AB,C=SE</b></p><p>for S/MIME certificate</p><p align=center><b>CN=UTN - DATACorp SGC,L=Salt Lake "
0577                    "City,SP=UT,OU=http://www.usertrust.com,O=The USERTRUST Network,C=US</b> (serial number 46EAF096054CC5E3FA65EA6E9F42C664)</p><p>expires in "
0578                    "5 days.</p>");
0579         QTest::newRow("certificate okay; issuer not checked") //
0580             << testKey("9E99817D12280C9677674430492EDA1DCE2E4C63", GpgME::CMS) //
0581             << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey} //
0582             << QDateTime{{2019, 6, 19}, {}, QTimeZone::UTC} // 5 days before expiration date of the issuer certificate
0583             << ExpiryChecker::NotNearExpiry //
0584             << days{346} //
0585             << 0 // issuer chain not checked
0586             << Key{} // ignored
0587             << ExpiryChecker::ExpirationStatus{} // ignored
0588             << days{} // ignored
0589             << 0 // expect 0 signal emission because certificate is not near expiry
0590             << QByteArray{} //
0591             << QString{};
0592         QTest::newRow("certificate near expiry; issuer expired") //
0593             << testKey("9E99817D12280C9677674430492EDA1DCE2E4C63", GpgME::CMS) //
0594             << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey | ExpiryChecker::CheckChain} //
0595             << QDateTime{{2020, 5, 25}, {}, QTimeZone::UTC} // 5 days before expiration date of the certificate
0596             << ExpiryChecker::ExpiresSoon //
0597             << days{5} //
0598             << 1 // one expired certificate in issuer chain
0599             << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) //
0600             << ExpiryChecker::Expired //
0601             << days{336} //
0602             << 2 // expect 2 signal emissions because both certificates in the 2-certificate chain are either expired or near expiry
0603             << QByteArray{"492EDA1DCE2E4C63"} // first signal emission references the certificate
0604             << QStringLiteral(
0605                    "<p>The S/MIME certificate for</p><p align=center><b>CN=UTN - DATACorp SGC,L=Salt Lake City,SP=UT,OU=http://www.usertrust.com,O=The "
0606                    "USERTRUST Network,C=US</b> (serial number 46EAF096054CC5E3FA65EA6E9F42C664)</p><p>expires in 5 days.</p>");
0607         QTest::newRow("certificate near expiry; issuer not checked")
0608             << testKey("9E99817D12280C9677674430492EDA1DCE2E4C63", GpgME::CMS) //
0609             << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey} //
0610             << QDateTime{{2020, 5, 25}, {}, QTimeZone::UTC} // 5 days before expiration date of the certificate
0611             << ExpiryChecker::ExpiresSoon //
0612             << days{5} //
0613             << 0 // issuer chain not checked
0614             << Key{} // ignored
0615             << ExpiryChecker::ExpirationStatus{} // ignored
0616             << days{} // ignored
0617             << 1 // expect 1 signal emission because certificate is near expiry
0618             << QByteArray{"492EDA1DCE2E4C63"} // first signal emission references the certificate
0619             << QStringLiteral(
0620                    "<p>The S/MIME certificate for</p><p align=center><b>CN=UTN - DATACorp SGC,L=Salt Lake City,SP=UT,OU=http://www.usertrust.com,O=The "
0621                    "USERTRUST Network,C=US</b> (serial number 46EAF096054CC5E3FA65EA6E9F42C664)</p><p>expires in 5 days.</p>");
0622     }
0623 
0624     void certificateChain()
0625     {
0626         QFETCH(GpgME::Key, key);
0627         QFETCH(ExpiryChecker::CheckFlags, checkFlags);
0628         QFETCH(QDateTime, fakedate);
0629         QFETCH(ExpiryChecker::ExpirationStatus, expectedStatus);
0630         QFETCH(Kleo::chrono::days, expectedDuration);
0631         QFETCH(int, expectedChainResults);
0632         QFETCH(GpgME::Key, expectedChainCertificate);
0633         QFETCH(ExpiryChecker::ExpirationStatus, expectedChainStatus);
0634         QFETCH(Kleo::chrono::days, expectedChainDuration);
0635         QFETCH(int, emissions);
0636         QFETCH(QByteArray, keyID);
0637         QFETCH(QString, msg);
0638 
0639         {
0640             ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{10}, days{10}, days{10}});
0641             checker.setTimeProviderForTest(std::make_shared<FakeTimeProvider>(fakedate));
0642             QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage);
0643             const auto result = checker.checkKey(key, checkFlags);
0644             QCOMPARE(result.checkFlags, checkFlags);
0645             QCOMPARE(result.expiration.certificate, key);
0646             QCOMPARE(result.expiration.status, expectedStatus);
0647             QCOMPARE(result.expiration.duration, expectedDuration);
0648             QCOMPARE(result.chainExpiration.size(), expectedChainResults);
0649             if (result.chainExpiration.size() > 0) {
0650                 const auto issuerExpiration = result.chainExpiration.front();
0651                 QCOMPARE(issuerExpiration.status, expectedChainStatus);
0652                 QCOMPARE(issuerExpiration.duration, expectedChainDuration);
0653             }
0654             QCOMPARE(spy.count(), emissions);
0655             if (emissions > 0) {
0656                 QList<QVariant> arguments = spy.takeFirst();
0657                 QCOMPARE(arguments.at(0).value<GpgME::Key>().keyID(), keyID);
0658                 QCOMPARE(arguments.at(1).toString(), msg);
0659                 QCOMPARE(arguments.at(2).value<ExpiryChecker::ExpiryInformation>(), ExpiryChecker::OtherKeyNearExpiry);
0660             }
0661         }
0662     }
0663 
0664     void noSuitableSubkey_data()
0665     {
0666         QTest::addColumn<GpgME::Key>("key");
0667         QTest::addColumn<ExpiryChecker::CheckFlags>("checkFlags");
0668 
0669         QTest::newRow("OpenPGP; no encryption subkey") //
0670             << testKey("sign-only@example.net", GpgME::OpenPGP) // sign-only key
0671             << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey};
0672         QTest::newRow("S/MIME; no encryption key") //
0673             << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) // certification-only key
0674             << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey};
0675         QTest::newRow("S/MIME; no signing key") //
0676             << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) // certification-only key
0677             << ExpiryChecker::CheckFlags{ExpiryChecker::SigningKey};
0678     }
0679 
0680     void noSuitableSubkey()
0681     {
0682         QFETCH(GpgME::Key, key);
0683         QFETCH(ExpiryChecker::CheckFlags, checkFlags);
0684 
0685         ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{1}, days{1}, days{1}});
0686         const auto result = checker.checkKey(key, checkFlags);
0687         QCOMPARE(result.expiration.certificate, key);
0688         QCOMPARE(result.expiration.status, ExpiryChecker::NoSuitableSubkey);
0689     }
0690 
0691 private:
0692     // OpenPGP keys
0693     //
0694     // pub   rsa2048 2009-11-13 [SC]
0695     //       1BA323932B3FAA826132C79E8D9860C58F246DE6
0696     // uid           [ultimate] unittest key (no password) <test@kolab.org>
0697     // sub   rsa2048 2009-11-13 [E]
0698     //
0699     // pub   ed25519 2019-01-22 [SC] [expired: 2021-01-21]
0700     //       EB85BB5FA33A75E15E944E63F231550C4F47E38E
0701     // uid           [ expired] alice@autocrypt.example
0702     // sub   cv25519 2019-01-22 [E] [expired: 2021-01-21]
0703     //
0704     // pub   ed25519 2023-04-17 [SC]
0705     //       C1218845DEEDA5432198FA7AF78A0834BB3C4A16
0706     // uid           [ultimate] encr-expires@example.net
0707     // sub   cv25519 2023-04-17 [E] [expires: 2023-04-27]
0708     //
0709     // pub   ed25519 2023-05-02 [SC] [expires: 2023-05-03]
0710     //       C3607CB03C13FDC6CB0384649358227B5DD4D260
0711     // uid           [ultimate] expires@example.net
0712     // sub   cv25519 2023-05-02 [E]
0713     //
0714     // pub   ed25519 2023-05-02 [SC] [expires: 2023-05-03]
0715     //       26C9EEEA094AC00FDA0FFC1384EFDDEEC99C022F
0716     // uid           [ultimate] sign-only@example.net
0717     //
0718     //
0719     // S/MIME certificates
0720     //
0721     //           ID: 0x212B49DC
0722     //          S/N: 00D345203A186385C9
0723     //        (dec): 15223609549285197257
0724     //       Issuer: /CN=unittest cert/O=KDAB/C=US/EMail=test@example.com
0725     //      Subject: /CN=unittest cert/O=KDAB/C=US/EMail=test@example.com
0726     //     validity: 2010-06-29 13:48:23 through 2013-03-25 13:48:23
0727     //     key type: rsa1024
0728     // chain length: unlimited
0729     //     sha1 fpr: 24:D2:FC:A2:2E:B3:B8:0A:1E:37:71:D1:4C:C6:58:E3:21:2B:49:DC
0730     //     sha2 fpr: 62:4B:A4:B8:7D:8F:99:AA:6B:46:E3:C8:C5:BE:BF:30:29:B6:EC:4E:CC:7D:1F:9F:A8:39:B6:CE:03:6F:C7:FB
0731     //
0732     // S/MIME certificates building a circular chain
0733     //
0734     //            ID: 0xBE231B05
0735     //           S/N: 51260A931CE27F9CC3A55F79E072AE82
0736     //         (dec): 107864989418777835411218143713715990146
0737     //        Issuer: /CN=UTN - DATACorp SGC/OU=http:\x2f\x2fwww.usertrust.com/O=The USERTRUST Network/L=Salt Lake City/ST=UT/C=US
0738     //       Subject: /CN=AddTrust External CA Root/OU=AddTrust External TTP Network/O=AddTrust AB/C=SE
0739     //      validity: 2005-06-07 08:09:10 through 2019-06-24 19:06:30
0740     //      key type: rsa2048
0741     //     key usage: certSign crlSign
0742     // ext key usage: ms-serverGatedCrypto (suggested), serverGatedCrypto.ns (suggested)
0743     //  chain length: unlimited
0744     //      sha1 fpr: 31:93:78:6A:48:BD:F2:D4:D2:0B:8F:C6:50:1F:4D:E8:BE:23:1B:05
0745     //      sha2 fpr: 92:5E:4B:37:2B:A3:2E:5E:87:30:22:84:B2:D7:C9:DF:BF:82:00:FF:CB:A0:D1:66:03:A1:A0:6F:F7:6C:D3:53
0746     //
0747     //            ID: 0xCE2E4C63
0748     //           S/N: 46EAF096054CC5E3FA65EA6E9F42C664
0749     //         (dec): 94265836834010752231943569188608722532
0750     //        Issuer: /CN=AddTrust External CA Root/OU=AddTrust External TTP Network/O=AddTrust AB/C=SE
0751     //       Subject: /CN=UTN - DATACorp SGC/OU=http:\x2f\x2fwww.usertrust.com/O=The USERTRUST Network/L=Salt Lake City/ST=UT/C=US
0752     //      validity: 2005-06-07 08:09:10 through 2020-05-30 10:48:38
0753     //      key type: rsa2048
0754     //     key usage: certSign crlSign
0755     // ext key usage: ms-serverGatedCrypto (suggested), serverGatedCrypto.ns (suggested)
0756     //      policies: 2.5.29.32.0:N:
0757     //  chain length: unlimited
0758     //      sha1 fpr: 9E:99:81:7D:12:28:0C:96:77:67:44:30:49:2E:DA:1D:CE:2E:4C:63
0759     //      sha2 fpr: 21:3F:AD:03:B1:C5:23:47:E9:A8:0F:29:9A:F0:89:9B:CA:FF:3F:62:B3:4E:B0:60:66:F4:D7:EE:A5:EE:1A:73
0760 
0761     Key testKey(const char *pattern, Protocol protocol = UnknownProtocol)
0762     {
0763         const std::vector<Key> keys = KeyCache::instance()->findByEMailAddress(pattern);
0764         for (const auto &key : keys) {
0765             if (protocol == UnknownProtocol || key.protocol() == protocol) {
0766                 return key;
0767             }
0768         }
0769         const auto key = KeyCache::instance()->findByKeyIDOrFingerprint(pattern);
0770         if (key.isNull()) {
0771             qWarning() << "No" << Formatting::displayName(protocol) << "test key found for" << pattern;
0772         }
0773         return key;
0774     }
0775 
0776 private:
0777     QSharedPointer<QTemporaryDir> mGnupgHome;
0778     std::shared_ptr<const KeyCache> mKeyCache;
0779 };
0780 
0781 QTEST_MAIN(ExpiryCheckerTest)
0782 #include "expirycheckertest.moc"