diff --git a/autotests/expirycheckertest.cpp b/autotests/expirycheckertest.cpp index a59c4ad07..611d6e950 100644 --- a/autotests/expirycheckertest.cpp +++ b/autotests/expirycheckertest.cpp @@ -1,683 +1,780 @@ /* This file is part of libkleopatra's test suite. SPDX-FileCopyrightText: 2022 Sandro Knauß SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: LGPL-2.0-or-later */ #include "testhelpers.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; using days = Kleo::chrono::days; class FakeTimeProvider : public Kleo::TimeProvider { public: explicit FakeTimeProvider(const QDateTime &dateTime) : mCurrentDate{dateTime.date()} , mCurrentTime{dateTime.toSecsSinceEpoch()} { } time_t currentTime() const override { return mCurrentTime; } QDate currentDate() const override { return mCurrentDate; } Qt::TimeSpec timeSpec() const override { // use UTC to avoid test failures caused by "wrong" local timezone return Qt::UTC; } private: QDate mCurrentDate; time_t mCurrentTime; }; class ExpiryCheckerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { qRegisterMetaType(); mGnupgHome = QTest::qExtractTestData(QStringLiteral("/fixtures/expirycheckertest")); qputenv("GNUPGHOME", mGnupgHome->path().toLocal8Bit()); // hold a reference to the key cache to avoid rebuilding while the test is running mKeyCache = KeyCache::instance(); // make sure that the key cache has been populated (void)mKeyCache->keys(); } void cleanupTestCase() { // verify that nobody else holds a reference to the key cache QVERIFY(mKeyCache.use_count() == 1); mKeyCache.reset(); (void)QProcess::execute(QStringLiteral("gpgconf"), {"--kill", "all"}); mGnupgHome.reset(); qunsetenv("GNUPGHOME"); } void errorHandling_data() { QTest::addColumn("key"); QTest::addColumn("checkFlags"); QTest::addColumn("expectedStatus"); QTest::newRow("invalid key") // << GpgME::Key{} // << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} // << ExpiryChecker::InvalidKey; QTest::newRow("invalid flags - no flags") // << testKey("test@kolab.org", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{} // << ExpiryChecker::InvalidCheckFlags; QTest::newRow("invalid flags - no usage flags") // << testKey("test@kolab.org", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::OwnKey | ExpiryChecker::CheckChain} // << ExpiryChecker::InvalidCheckFlags; } void errorHandling() { QFETCH(GpgME::Key, key); QFETCH(ExpiryChecker::CheckFlags, checkFlags); QFETCH(ExpiryChecker::ExpirationStatus, expectedStatus); ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{1}, days{1}, days{1}}); QTest::ignoreMessage(QtWarningMsg, QRegularExpression{QStringLiteral("checkKey called with")}); const auto result = checker.checkKey(key, checkFlags); QCOMPARE(result.expiration.certificate, key); QCOMPARE(result.expiration.status, expectedStatus); } void valid_data() { QTest::addColumn("key"); QTest::addColumn("fakedate"); // use dates between creation date and expiration date (if there is one) of the test keys/certificates QTest::newRow("neverExpire") << testKey("test@kolab.org", GpgME::OpenPGP) << QDateTime{{2012, 1, 1}, {}, Qt::UTC}; QTest::newRow("openpgp") << testKey("alice@autocrypt.example", GpgME::OpenPGP) << QDateTime{{2020, 1, 1}, {}, Qt::UTC}; QTest::newRow("smime") << testKey("test@example.com", GpgME::CMS) << QDateTime{{2012, 1, 1}, {}, Qt::UTC}; } void valid() { QFETCH(GpgME::Key, key); QFETCH(QDateTime, fakedate); ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{1}, days{1}, days{1}}); checker.setTimeProviderForTest(std::make_shared(fakedate)); QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); const auto result = checker.checkKey(key, ExpiryChecker::EncryptionKey); QCOMPARE(result.checkFlags, ExpiryChecker::EncryptionKey); QCOMPARE(result.expiration.certificate, key); QCOMPARE(result.expiration.status, ExpiryChecker::NotNearExpiry); QCOMPARE(spy.count(), 0); } void expired_data() { QTest::addColumn("key"); QTest::addColumn("checkFlags"); QTest::addColumn("fakedate"); QTest::addColumn("expectedDuration"); QTest::addColumn("expiryInfo"); QTest::addColumn("msg"); QTest::newRow("openpgp - other; 0 days ago") // << testKey("alice@autocrypt.example", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} // << QDateTime{{2021, 1, 21}, {23, 59, 59}, Qt::UTC} // the last second of the day the key expired << days{0} // << ExpiryChecker::OtherKeyExpired << QStringLiteral( "

The OpenPGP key for

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expired less than a day " "ago.

"); QTest::newRow("openpgp - own; 1 day ago") // << testKey("alice@autocrypt.example", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} // << QDateTime{{2021, 1, 22}, {}, Qt::UTC} // the day after the expiration date of the key << days{1} // << ExpiryChecker::OwnKeyExpired << QStringLiteral( "

Your OpenPGP encryption key

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expired yesterday.

"); QTest::newRow("openpgp - own signing; 2 days ago") // << testKey("alice@autocrypt.example", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} // << QDateTime{{2021, 1, 23}, {}, Qt::UTC} // the second day after the expiration date of the key << days{2} // << ExpiryChecker::OwnKeyExpired << QStringLiteral( "

Your OpenPGP signing key

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expired 2 days " "ago.

"); QTest::newRow("smime - other; 0 days ago") // << testKey("test@example.com", GpgME::CMS) // << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} // << QDateTime{{2013, 3, 25}, {23, 59, 59}, Qt::UTC} // the last second of the day the key expired << days{0} // << ExpiryChecker::OtherKeyExpired << QStringLiteral( "

The S/MIME certificate for

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US (serial " "number 00D345203A186385C9)

expired less than a day ago.

"); QTest::newRow("smime - own; 1 day ago") // << testKey("test@example.com", GpgME::CMS) // << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} // << QDateTime{{2013, 3, 26}, {}, Qt::UTC} // the day after the expiration date of the key << days{1} // << ExpiryChecker::OwnKeyExpired << QStringLiteral( "

Your S/MIME encryption certificate

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US " "(serial number 00D345203A186385C9)

expired yesterday.

"); QTest::newRow("smime - own signing; 2 days ago") // << testKey("test@example.com", GpgME::CMS) // << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} // << QDateTime{{2013, 3, 27}, {}, Qt::UTC} // the second day after the expiration date of the key << days{2} // << ExpiryChecker::OwnKeyExpired << QStringLiteral( "

Your S/MIME signing certificate

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US " "(serial number 00D345203A186385C9)

expired 2 days ago.

"); } void expired() { QFETCH(GpgME::Key, key); QFETCH(ExpiryChecker::CheckFlags, checkFlags); QFETCH(QDateTime, fakedate); QFETCH(Kleo::chrono::days, expectedDuration); QFETCH(ExpiryChecker::ExpiryInformation, expiryInfo); QFETCH(QString, msg); { ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{1}, days{1}, days{1}}); checker.setTimeProviderForTest(std::make_shared(fakedate)); QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); const auto result = checker.checkKey(key, checkFlags); QCOMPARE(result.checkFlags, checkFlags); QCOMPARE(result.expiration.certificate, key); QCOMPARE(result.expiration.status, ExpiryChecker::Expired); QCOMPARE(result.expiration.duration, expectedDuration); QCOMPARE(spy.count(), 1); QList arguments = spy.takeFirst(); QCOMPARE(arguments.at(0).value().keyID(), key.keyID()); QCOMPARE(arguments.at(1).toString(), msg); QCOMPARE(arguments.at(2).value(), expiryInfo); } } void nearexpiry_data() { QTest::addColumn("key"); QTest::addColumn("fakedate"); QTest::addColumn("expectedDuration"); QTest::addColumn("msg"); QTest::addColumn("msgOwnKey"); QTest::addColumn("msgOwnSigningKey"); // use the day 5 days before the expiration date of the test keys/certificates as fake date QTest::newRow("openpgp") << testKey("alice@autocrypt.example", GpgME::OpenPGP) // << QDateTime{{2021, 1, 16}, {}, Qt::UTC} // << days{5} << QStringLiteral( "

The OpenPGP key for

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expires in 5 days.

") << QStringLiteral( "

Your OpenPGP encryption key

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expires in 5 " "days.

") << QStringLiteral( "

Your OpenPGP signing key

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expires in 5 " "days.

"); QTest::newRow("smime") << testKey("test@example.com", GpgME::CMS) // << QDateTime{{2013, 3, 20}, {}, Qt::UTC} // << days{5} << QStringLiteral( "

The S/MIME certificate for

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US (serial " "number 00D345203A186385C9)

expires in 5 days.

") << QStringLiteral( "

Your S/MIME encryption certificate

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US " "(serial number 00D345203A186385C9)

expires in 5 days.

") << QStringLiteral( "

Your S/MIME signing certificate

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US " "(serial number 00D345203A186385C9)

expires in 5 days.

"); } void nearexpiry() { QFETCH(GpgME::Key, key); QFETCH(QDateTime, fakedate); QFETCH(Kleo::chrono::days, expectedDuration); QFETCH(QString, msg); QFETCH(QString, msgOwnKey); QFETCH(QString, msgOwnSigningKey); { ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{10}, days{1}, days{1}}); checker.setTimeProviderForTest(std::make_shared(fakedate)); QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); // Test if the correct threshold is taken { const auto result = checker.checkKey(key, ExpiryChecker::EncryptionKey); QCOMPARE(result.checkFlags, ExpiryChecker::EncryptionKey); QCOMPARE(result.expiration.certificate, key); QCOMPARE(result.expiration.status, ExpiryChecker::ExpiresSoon); QCOMPARE(result.expiration.duration, expectedDuration); QCOMPARE(spy.count(), 1); } { const auto result = checker.checkKey(key, ExpiryChecker::OwnEncryptionKey); QCOMPARE(result.checkFlags, ExpiryChecker::OwnEncryptionKey); QCOMPARE(result.expiration.certificate, key); QCOMPARE(result.expiration.status, ExpiryChecker::NotNearExpiry); QCOMPARE(result.expiration.duration, expectedDuration); QCOMPARE(spy.count(), 1); } { const auto result = checker.checkKey(key, ExpiryChecker::OwnSigningKey); QCOMPARE(result.checkFlags, ExpiryChecker::OwnSigningKey); QCOMPARE(result.expiration.certificate, key); QCOMPARE(result.expiration.status, ExpiryChecker::NotNearExpiry); QCOMPARE(result.expiration.duration, expectedDuration); QCOMPARE(spy.count(), 1); } QList arguments = spy.takeFirst(); QCOMPARE(arguments.at(0).value().keyID(), key.keyID()); QCOMPARE(arguments.at(1).toString(), msg); QCOMPARE(arguments.at(2).value(), ExpiryChecker::OtherKeyNearExpiry); } { ExpiryChecker checker(ExpiryCheckerSettings{days{10}, days{1}, days{1}, days{1}}); checker.setTimeProviderForTest(std::make_shared(fakedate)); QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); // Test if the correct treshold is taken checker.checkKey(key, ExpiryChecker::EncryptionKey); checker.checkKey(key, ExpiryChecker::OwnEncryptionKey); QCOMPARE(spy.count(), 1); QList arguments = spy.takeFirst(); QCOMPARE(arguments.at(0).value().keyID(), key.keyID()); QCOMPARE(arguments.at(1).toString(), msgOwnKey); QCOMPARE(arguments.at(2).value(), ExpiryChecker::OwnKeyNearExpiry); } { ExpiryChecker checker(ExpiryCheckerSettings{days{10}, days{1}, days{1}, days{1}}); checker.setTimeProviderForTest(std::make_shared(fakedate)); QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); // Test if the correct treshold is taken checker.checkKey(key, ExpiryChecker::EncryptionKey); checker.checkKey(key, ExpiryChecker::OwnSigningKey); QCOMPARE(spy.count(), 1); QList arguments = spy.takeFirst(); QCOMPARE(arguments.at(0).value().keyID(), key.keyID()); QCOMPARE(arguments.at(1).toString(), msgOwnSigningKey); QCOMPARE(arguments.at(2).value(), ExpiryChecker::OwnKeyNearExpiry); } } void expiringEncryptionSubkey_data() { QTest::addColumn("key"); QTest::addColumn("checkFlags"); QTest::addColumn("fakedate"); QTest::addColumn("expectedStatus"); QTest::addColumn("expectedDuration"); QTest::newRow("valid - sign") // << testKey("encr-expires@example.net", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} // << QDateTime{{2023, 4, 18}, {}, Qt::UTC} // 9 days before expiration of encryption subkey << ExpiryChecker::NotNearExpiry // << days{0}; // ignored QTest::newRow("valid - encrypt to self") // << testKey("encr-expires@example.net", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} // << QDateTime{{2023, 4, 18}, {}, Qt::UTC} // 9 days before expiration of encryption subkey << ExpiryChecker::NotNearExpiry // << days{0}; // ignored QTest::newRow("valid - encrypt to others") // << testKey("encr-expires@example.net", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} // << QDateTime{{2023, 4, 18}, {}, Qt::UTC} // 9 days before expiration of encryption subkey << ExpiryChecker::NotNearExpiry // << days{0}; // ignored QTest::newRow("near expiry - sign") // << testKey("encr-expires@example.net", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} // << QDateTime{{2023, 4, 26}, {}, Qt::UTC} // 1 day before expiration of encryption subkey << ExpiryChecker::NotNearExpiry // signing key doesn't expire << days{0}; // ignored QTest::newRow("near expiry - encrypt to self") // << testKey("encr-expires@example.net", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} // << QDateTime{{2023, 4, 26}, {}, Qt::UTC} // 1 day before expiration of encryption subkey << ExpiryChecker::ExpiresSoon // << days{1}; QTest::newRow("near expiry - encrypt to others") // << testKey("encr-expires@example.net", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} // << QDateTime{{2023, 4, 26}, {}, Qt::UTC} // 1 day before expiration of encryption subkey << ExpiryChecker::ExpiresSoon // << days{1}; QTest::newRow("expired - sign") // << testKey("encr-expires@example.net", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} // << QDateTime{{2023, 4, 28}, {}, Qt::UTC} // 1 day after expiration of encryption subkey << ExpiryChecker::NotNearExpiry // signing key doesn't expire << days{0}; // ignored QTest::newRow("expired - encrypt to self") // << testKey("encr-expires@example.net", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} // << QDateTime{{2023, 4, 28}, {}, Qt::UTC} // 1 day after expiration of encryption subkey << ExpiryChecker::Expired // << days{1}; QTest::newRow("expired - encrypt to others") // << testKey("encr-expires@example.net", GpgME::OpenPGP) // << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} // << QDateTime{{2023, 4, 28}, {}, Qt::UTC} // 1 day after expiration of encryption subkey << ExpiryChecker::Expired // << days{1}; } void expiringEncryptionSubkey() { QFETCH(GpgME::Key, key); QFETCH(ExpiryChecker::CheckFlags, checkFlags); QFETCH(QDateTime, fakedate); QFETCH(ExpiryChecker::ExpirationStatus, expectedStatus); QFETCH(Kleo::chrono::days, expectedDuration); ExpiryChecker checker(ExpiryCheckerSettings{days{5}, days{5}, days{5}, days{5}}); checker.setTimeProviderForTest(std::make_shared(fakedate)); const auto result = checker.checkKey(key, checkFlags); QCOMPARE(result.checkFlags, checkFlags); QCOMPARE(result.expiration.certificate, key); QCOMPARE(result.expiration.status, expectedStatus); if (expectedStatus != ExpiryChecker::NotNearExpiry) { // duration is undefined if status is NotNearExpiry QCOMPARE(result.expiration.duration, expectedDuration); } } + void notExpiringEncryptionSubkey_data() + { + QTest::addColumn("key"); + QTest::addColumn("checkFlags"); + QTest::addColumn("fakedate"); + QTest::addColumn("expectedStatus"); + QTest::addColumn("expectedDuration"); + + QTest::newRow("valid - sign") // + << testKey("expires@example.net", GpgME::OpenPGP) // + << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} // + << QDateTime{{2023, 4, 24}, {}, Qt::UTC} // 9 days before expiration of primary key + << ExpiryChecker::NotNearExpiry // + << days{0}; // ignored + QTest::newRow("valid - encrypt to self") // + << testKey("expires@example.net", GpgME::OpenPGP) // + << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} // + << QDateTime{{2023, 4, 24}, {}, Qt::UTC} // 9 days before expiration of primary key + << ExpiryChecker::NotNearExpiry // + << days{0}; // ignored + QTest::newRow("valid - encrypt to others") // + << testKey("expires@example.net", GpgME::OpenPGP) // + << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} // + << QDateTime{{2023, 4, 24}, {}, Qt::UTC} // 9 days before expiration of primary key + << ExpiryChecker::NotNearExpiry // + << days{0}; // ignored + QTest::newRow("near expiry - sign") // + << testKey("expires@example.net", GpgME::OpenPGP) // + << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} // + << QDateTime{{2023, 5, 2}, {}, Qt::UTC} // 1 day before expiration of primary key + << ExpiryChecker::ExpiresSoon // + << days{1}; + QTest::newRow("near expiry - encrypt to self") // + << testKey("expires@example.net", GpgME::OpenPGP) // + << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} // + << QDateTime{{2023, 5, 2}, {}, Qt::UTC} // 1 day before expiration of primary key + << ExpiryChecker::ExpiresSoon // + << days{1}; + QTest::newRow("near expiry - encrypt to others") // + << testKey("expires@example.net", GpgME::OpenPGP) // + << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} // + << QDateTime{{2023, 5, 2}, {}, Qt::UTC} // 1 day before expiration of primary key + << ExpiryChecker::ExpiresSoon // + << days{1}; + QTest::newRow("expired - sign") // + << testKey("expires@example.net", GpgME::OpenPGP) // + << ExpiryChecker::CheckFlags{ExpiryChecker::OwnSigningKey} // + << QDateTime{{2023, 5, 4}, {}, Qt::UTC} // 1 day after expiration of primary key + << ExpiryChecker::Expired // + << days{1}; + QTest::newRow("expired - encrypt to self") // + << testKey("expires@example.net", GpgME::OpenPGP) // + << ExpiryChecker::CheckFlags{ExpiryChecker::OwnEncryptionKey} // + << QDateTime{{2023, 5, 4}, {}, Qt::UTC} // 1 day after expiration of primary key + << ExpiryChecker::Expired // + << days{1}; + QTest::newRow("expired - encrypt to others") // + << testKey("expires@example.net", GpgME::OpenPGP) // + << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey} // + << QDateTime{{2023, 5, 4}, {}, Qt::UTC} // 1 day after expiration of primary key + << ExpiryChecker::Expired // + << days{1}; + } + + void notExpiringEncryptionSubkey() + { + QFETCH(GpgME::Key, key); + QFETCH(ExpiryChecker::CheckFlags, checkFlags); + QFETCH(QDateTime, fakedate); + QFETCH(ExpiryChecker::ExpirationStatus, expectedStatus); + QFETCH(Kleo::chrono::days, expectedDuration); + + ExpiryChecker checker(ExpiryCheckerSettings{days{5}, days{5}, days{5}, days{5}}); + checker.setTimeProviderForTest(std::make_shared(fakedate)); + const auto result = checker.checkKey(key, checkFlags); + QCOMPARE(result.checkFlags, checkFlags); + QCOMPARE(result.expiration.certificate, key); + QCOMPARE(result.expiration.status, expectedStatus); + if (expectedStatus != ExpiryChecker::NotNearExpiry) { + // duration is undefined if status is NotNearExpiry + QCOMPARE(result.expiration.duration, expectedDuration); + } + } + void certificateChain_data() { QTest::addColumn("key"); QTest::addColumn("checkFlags"); QTest::addColumn("fakedate"); QTest::addColumn("expectedStatus"); QTest::addColumn("expectedDuration"); QTest::addColumn("expectedChainResults"); QTest::addColumn("expectedChainCertificate"); QTest::addColumn("expectedChainStatus"); QTest::addColumn("expectedChainDuration"); QTest::addColumn("emissions"); QTest::addColumn("keyID"); QTest::addColumn("msg"); QTest::newRow("certificate near expiry; issuer okay") // << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) // << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey | ExpiryChecker::CheckChain} // << QDateTime{{2019, 6, 19}, {}, Qt::UTC} // 5 days before expiration date of the certificate << ExpiryChecker::ExpiresSoon // << days{5} // << 0 // no expired or expiring certificates in issuer chain << Key{} // ignored << ExpiryChecker::ExpirationStatus{} // ignored << days{} // ignored << 1 // expect 1 signal emission because of a 2-certificate chain with 1 cert near expiry << QByteArray{"501F4DE8BE231B05"} // first signal emission references the certificate << QStringLiteral( "

The S/MIME certificate for

CN=AddTrust External CA Root,OU=AddTrust External TTP Network,O=AddTrust AB,C=SE " "(serial number 51260A931CE27F9CC3A55F79E072AE82)

expires in 5 days.

"); QTest::newRow("certificate near expiry; issuer not checked") // << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) // << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey} // << QDateTime{{2019, 6, 19}, {}, Qt::UTC} // 5 days before expiration date of the certificate << ExpiryChecker::ExpiresSoon // << days{5} // << 0 // issuer chain not checked << Key{} // ignored << ExpiryChecker::ExpirationStatus{} // ignored << days{} // ignored << 1 // expect 1 signal emission because certificate is near expiry << QByteArray{"501F4DE8BE231B05"} // signal emission references the certificate << QStringLiteral( "

The S/MIME certificate for

CN=AddTrust External CA Root,OU=AddTrust External TTP Network,O=AddTrust AB,C=SE " "(serial number 51260A931CE27F9CC3A55F79E072AE82)

expires in 5 days.

"); QTest::newRow("certificate okay; issuer near expiry") // << testKey("9E99817D12280C9677674430492EDA1DCE2E4C63", GpgME::CMS) // << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey | ExpiryChecker::CheckChain} // << QDateTime{{2019, 6, 19}, {}, Qt::UTC} // 5 days before expiration date of the issuer certificate << ExpiryChecker::NotNearExpiry // << days{346} // << 1 // one expiring certificate in issuer chain << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) // << ExpiryChecker::ExpiresSoon // << days{5} // << 1 // expect 1 signal emission because of a 2-certificate chain with 1 cert near expiry << QByteArray{"501F4DE8BE231B05"} // first signal emission references the isser certificate << QStringLiteral( "

The intermediate CA certificate

CN=AddTrust External CA Root,OU=AddTrust External TTP Network,O=AddTrust " "AB,C=SE

for S/MIME certificate

CN=UTN - DATACorp SGC,L=Salt Lake " "City,SP=UT,OU=http://www.usertrust.com,O=The USERTRUST Network,C=US (serial number 46EAF096054CC5E3FA65EA6E9F42C664)

expires in " "5 days.

"); QTest::newRow("certificate okay; issuer not checked") // << testKey("9E99817D12280C9677674430492EDA1DCE2E4C63", GpgME::CMS) // << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey} // << QDateTime{{2019, 6, 19}, {}, Qt::UTC} // 5 days before expiration date of the issuer certificate << ExpiryChecker::NotNearExpiry // << days{346} // << 0 // issuer chain not checked << Key{} // ignored << ExpiryChecker::ExpirationStatus{} // ignored << days{} // ignored << 0 // expect 0 signal emission because certificate is not near expiry << QByteArray{} // << QString{}; QTest::newRow("certificate near expiry; issuer expired") // << testKey("9E99817D12280C9677674430492EDA1DCE2E4C63", GpgME::CMS) // << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey | ExpiryChecker::CheckChain} // << QDateTime{{2020, 5, 25}, {}, Qt::UTC} // 5 days before expiration date of the certificate << ExpiryChecker::ExpiresSoon // << days{5} // << 1 // one expired certificate in issuer chain << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) // << ExpiryChecker::Expired // << days{336} // << 2 // expect 2 signal emissions because both certificates in the 2-certificate chain are either expired or near expiry << QByteArray{"492EDA1DCE2E4C63"} // first signal emission references the certificate << QStringLiteral( "

The S/MIME certificate for

CN=UTN - DATACorp SGC,L=Salt Lake City,SP=UT,OU=http://www.usertrust.com,O=The " "USERTRUST Network,C=US (serial number 46EAF096054CC5E3FA65EA6E9F42C664)

expires in 5 days.

"); QTest::newRow("certificate near expiry; issuer not checked") << testKey("9E99817D12280C9677674430492EDA1DCE2E4C63", GpgME::CMS) // << ExpiryChecker::CheckFlags{ExpiryChecker::CertificationKey} // << QDateTime{{2020, 5, 25}, {}, Qt::UTC} // 5 days before expiration date of the certificate << ExpiryChecker::ExpiresSoon // << days{5} // << 0 // issuer chain not checked << Key{} // ignored << ExpiryChecker::ExpirationStatus{} // ignored << days{} // ignored << 1 // expect 1 signal emission because certificate is near expiry << QByteArray{"492EDA1DCE2E4C63"} // first signal emission references the certificate << QStringLiteral( "

The S/MIME certificate for

CN=UTN - DATACorp SGC,L=Salt Lake City,SP=UT,OU=http://www.usertrust.com,O=The " "USERTRUST Network,C=US (serial number 46EAF096054CC5E3FA65EA6E9F42C664)

expires in 5 days.

"); } void certificateChain() { QFETCH(GpgME::Key, key); QFETCH(ExpiryChecker::CheckFlags, checkFlags); QFETCH(QDateTime, fakedate); QFETCH(ExpiryChecker::ExpirationStatus, expectedStatus); QFETCH(Kleo::chrono::days, expectedDuration); QFETCH(int, expectedChainResults); QFETCH(GpgME::Key, expectedChainCertificate); QFETCH(ExpiryChecker::ExpirationStatus, expectedChainStatus); QFETCH(Kleo::chrono::days, expectedChainDuration); QFETCH(int, emissions); QFETCH(QByteArray, keyID); QFETCH(QString, msg); { ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{10}, days{10}, days{10}}); checker.setTimeProviderForTest(std::make_shared(fakedate)); QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); const auto result = checker.checkKey(key, checkFlags); QCOMPARE(result.checkFlags, checkFlags); QCOMPARE(result.expiration.certificate, key); QCOMPARE(result.expiration.status, expectedStatus); QCOMPARE(result.expiration.duration, expectedDuration); QCOMPARE(result.chainExpiration.size(), expectedChainResults); if (result.chainExpiration.size() > 0) { const auto issuerExpiration = result.chainExpiration.front(); QCOMPARE(issuerExpiration.status, expectedChainStatus); QCOMPARE(issuerExpiration.duration, expectedChainDuration); } QCOMPARE(spy.count(), emissions); if (emissions > 0) { QList arguments = spy.takeFirst(); QCOMPARE(arguments.at(0).value().keyID(), keyID); QCOMPARE(arguments.at(1).toString(), msg); QCOMPARE(arguments.at(2).value(), ExpiryChecker::OtherKeyNearExpiry); } } } void noSuitableSubkey_data() { QTest::addColumn("key"); QTest::addColumn("checkFlags"); - QTest::newRow("no encryption (sub)key") // + QTest::newRow("OpenPGP; no encryption subkey") // + << testKey("sign-only@example.net", GpgME::OpenPGP) // sign-only key + << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey}; + QTest::newRow("S/MIME; no encryption key") // << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) // certification-only key << ExpiryChecker::CheckFlags{ExpiryChecker::EncryptionKey}; - QTest::newRow("no signing (sub)key") // + QTest::newRow("S/MIME; no signing key") // << testKey("3193786A48BDF2D4D20B8FC6501F4DE8BE231B05", GpgME::CMS) // certification-only key << ExpiryChecker::CheckFlags{ExpiryChecker::SigningKey}; } void noSuitableSubkey() { QFETCH(GpgME::Key, key); QFETCH(ExpiryChecker::CheckFlags, checkFlags); ExpiryChecker checker(ExpiryCheckerSettings{days{1}, days{1}, days{1}, days{1}}); const auto result = checker.checkKey(key, checkFlags); QCOMPARE(result.expiration.certificate, key); QCOMPARE(result.expiration.status, ExpiryChecker::NoSuitableSubkey); } private: // OpenPGP keys // // pub rsa2048 2009-11-13 [SC] // 1BA323932B3FAA826132C79E8D9860C58F246DE6 // uid [ultimate] unittest key (no password) // sub rsa2048 2009-11-13 [E] // // pub ed25519 2019-01-22 [SC] [expired: 2021-01-21] // EB85BB5FA33A75E15E944E63F231550C4F47E38E // uid [ expired] alice@autocrypt.example + // sub cv25519 2019-01-22 [E] [expired: 2021-01-21] // // pub ed25519 2023-04-17 [SC] - // C1218845DEEDA5432198FA7AF78A0834BB3C4A16 + // C1218845DEEDA5432198FA7AF78A0834BB3C4A16 // uid [ultimate] encr-expires@example.net // sub cv25519 2023-04-17 [E] [expires: 2023-04-27] // + // pub ed25519 2023-05-02 [SC] [expires: 2023-05-03] + // C3607CB03C13FDC6CB0384649358227B5DD4D260 + // uid [ultimate] expires@example.net + // sub cv25519 2023-05-02 [E] + // + // pub ed25519 2023-05-02 [SC] [expires: 2023-05-03] + // 26C9EEEA094AC00FDA0FFC1384EFDDEEC99C022F + // uid [ultimate] sign-only@example.net + // // // S/MIME certificates // // ID: 0x212B49DC // S/N: 00D345203A186385C9 // (dec): 15223609549285197257 // Issuer: /CN=unittest cert/O=KDAB/C=US/EMail=test@example.com // Subject: /CN=unittest cert/O=KDAB/C=US/EMail=test@example.com // validity: 2010-06-29 13:48:23 through 2013-03-25 13:48:23 // key type: rsa1024 // chain length: unlimited // sha1 fpr: 24:D2:FC:A2:2E:B3:B8:0A:1E:37:71:D1:4C:C6:58:E3:21:2B:49:DC // 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 // // S/MIME certificates building a circular chain // // ID: 0xBE231B05 // S/N: 51260A931CE27F9CC3A55F79E072AE82 // (dec): 107864989418777835411218143713715990146 // Issuer: /CN=UTN - DATACorp SGC/OU=http:\x2f\x2fwww.usertrust.com/O=The USERTRUST Network/L=Salt Lake City/ST=UT/C=US // Subject: /CN=AddTrust External CA Root/OU=AddTrust External TTP Network/O=AddTrust AB/C=SE // validity: 2005-06-07 08:09:10 through 2019-06-24 19:06:30 // key type: rsa2048 // key usage: certSign crlSign // ext key usage: ms-serverGatedCrypto (suggested), serverGatedCrypto.ns (suggested) // chain length: unlimited // sha1 fpr: 31:93:78:6A:48:BD:F2:D4:D2:0B:8F:C6:50:1F:4D:E8:BE:23:1B:05 // 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 // // ID: 0xCE2E4C63 // S/N: 46EAF096054CC5E3FA65EA6E9F42C664 // (dec): 94265836834010752231943569188608722532 // Issuer: /CN=AddTrust External CA Root/OU=AddTrust External TTP Network/O=AddTrust AB/C=SE // Subject: /CN=UTN - DATACorp SGC/OU=http:\x2f\x2fwww.usertrust.com/O=The USERTRUST Network/L=Salt Lake City/ST=UT/C=US // validity: 2005-06-07 08:09:10 through 2020-05-30 10:48:38 // key type: rsa2048 // key usage: certSign crlSign // ext key usage: ms-serverGatedCrypto (suggested), serverGatedCrypto.ns (suggested) // policies: 2.5.29.32.0:N: // chain length: unlimited // sha1 fpr: 9E:99:81:7D:12:28:0C:96:77:67:44:30:49:2E:DA:1D:CE:2E:4C:63 // 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 Key testKey(const char *pattern, Protocol protocol = UnknownProtocol) { const std::vector keys = KeyCache::instance()->findByEMailAddress(pattern); for (const auto &key : keys) { if (protocol == UnknownProtocol || key.protocol() == protocol) { return key; } } const auto key = KeyCache::instance()->findByKeyIDOrFingerprint(pattern); if (key.isNull()) { qWarning() << "No" << Formatting::displayName(protocol) << "test key found for" << pattern; } return key; } private: QSharedPointer mGnupgHome; std::shared_ptr mKeyCache; }; QTEST_MAIN(ExpiryCheckerTest) #include "expirycheckertest.moc" diff --git a/autotests/fixtures/expirycheckertest/private-keys-v1.d/4CA3595FF38DEF035B2D0158CF68EDE4DCC46868.key b/autotests/fixtures/expirycheckertest/private-keys-v1.d/4CA3595FF38DEF035B2D0158CF68EDE4DCC46868.key new file mode 100644 index 000000000..365952446 --- /dev/null +++ b/autotests/fixtures/expirycheckertest/private-keys-v1.d/4CA3595FF38DEF035B2D0158CF68EDE4DCC46868.key @@ -0,0 +1,5 @@ +Created: 20230502T122844 +Key: (private-key (ecc (curve Ed25519)(flags eddsa)(q + #40AECE18FBDD00744B9D4F6D3509E1A79D7499C2D1D313574FD602A793F89A6BF8#) + (d #17FD23A6FA5840326B770DABAC8221F50D20B9D8D6F60D70E6EC69584177170D#) + )) diff --git a/autotests/fixtures/expirycheckertest/private-keys-v1.d/813F09E28E5F03D348AE525DE0F40BC5D2F512E9.key b/autotests/fixtures/expirycheckertest/private-keys-v1.d/813F09E28E5F03D348AE525DE0F40BC5D2F512E9.key new file mode 100644 index 000000000..ad38ca7b0 --- /dev/null +++ b/autotests/fixtures/expirycheckertest/private-keys-v1.d/813F09E28E5F03D348AE525DE0F40BC5D2F512E9.key @@ -0,0 +1,5 @@ +Created: 20230502T122844 +Key: (private-key (ecc (curve Curve25519)(flags djb-tweak)(q + #40C4F4A5A4232D0872E8A3746EF146D7AC43259A632D61AA269ED1A85CE9865F5F#) + (d #7BC05FD6CC8525F1249438562A0BC9B26B538CD8008C73A7AA6B67514F256898#) + )) diff --git a/autotests/fixtures/expirycheckertest/private-keys-v1.d/F466487705518FB398FA38EA0FE10B131CAA3D3F.key b/autotests/fixtures/expirycheckertest/private-keys-v1.d/F466487705518FB398FA38EA0FE10B131CAA3D3F.key new file mode 100644 index 000000000..e5d64cc3c --- /dev/null +++ b/autotests/fixtures/expirycheckertest/private-keys-v1.d/F466487705518FB398FA38EA0FE10B131CAA3D3F.key @@ -0,0 +1,5 @@ +Created: 20230502T125008 +Key: (private-key (ecc (curve Ed25519)(flags eddsa)(q + #40A122699DC82CE2C8B874F42E468745E092D827FC465340FAEC862D4AC40FA999#) + (d #BC8C49C226DEE5BA2BB0024C53482755CE077E7C2092EA647679F87400F2E057#) + )) diff --git a/autotests/fixtures/expirycheckertest/pubring.kbx b/autotests/fixtures/expirycheckertest/pubring.kbx index a83bb41d1..8e57f438e 100644 Binary files a/autotests/fixtures/expirycheckertest/pubring.kbx and b/autotests/fixtures/expirycheckertest/pubring.kbx differ diff --git a/autotests/fixtures/expirycheckertest/trustdb.gpg b/autotests/fixtures/expirycheckertest/trustdb.gpg index 807e17efb..2bf603bd5 100644 Binary files a/autotests/fixtures/expirycheckertest/trustdb.gpg and b/autotests/fixtures/expirycheckertest/trustdb.gpg differ diff --git a/src/kleo/expirychecker.cpp b/src/kleo/expirychecker.cpp index a2640b6a6..7e10d5b10 100644 --- a/src/kleo/expirychecker.cpp +++ b/src/kleo/expirychecker.cpp @@ -1,540 +1,541 @@ /* This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 Sandro Knauß SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker Based on kpgp.h Copyright (C) 2001,2002 the KPGP authors See file libkdenetwork/AUTHORS.kpgp for details SPDX-License-Identifier: LGPL-2.0-or-later */ #include "expirychecker.h" #include "debug.h" #include "dn.h" #include "expirycheckersettings.h" #include #include #include #include #include #include #include #include #include #include using namespace Kleo; class Kleo::ExpiryCheckerPrivate { Kleo::ExpiryChecker *q; public: ExpiryCheckerPrivate(ExpiryChecker *qq, const ExpiryCheckerSettings &settings_) : q{qq} , settings{settings_} { } ExpiryChecker::Expiration calculateExpiration(const GpgME::Subkey &subkey) const; ExpiryChecker::Expiration checkForExpiration(const GpgME::Key &key, Kleo::chrono::days threshold, ExpiryChecker::CheckFlags flags) const; ExpiryChecker::Result checkKeyNearExpiry(const GpgME::Key &key, ExpiryChecker::CheckFlags flags); ExpiryCheckerSettings settings; std::set alreadyWarnedFingerprints; std::shared_ptr timeProvider; }; ExpiryChecker::ExpiryChecker(const ExpiryCheckerSettings &settings, QObject *parent) : QObject{parent} , d{new ExpiryCheckerPrivate{this, settings}} { } ExpiryChecker::~ExpiryChecker() = default; ExpiryCheckerSettings ExpiryChecker::settings() const { return d->settings; } QString formatOpenPGPMessage(ExpiryChecker::Expiration expiration, ExpiryChecker::CheckFlags flags) { const GpgME::Key key = expiration.certificate; const bool isOwnKey = flags & ExpiryChecker::OwnKey; const bool isSigningKey = flags & ExpiryChecker::SigningKey; const auto keyInfo = ki18nc("User ID of key (KeyID key ID of key in hex notation)", "%1 (KeyID 0x%2)") .subs(QString::fromUtf8(key.userID(0).id())) .subs(QString::fromLatin1(key.keyID())); if (expiration.status == ExpiryChecker::Expired) { qCDebug(LIBKLEO_LOG) << "Key" << key << "expired" << expiration.duration.count() << "days ago"; if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n("

Your OpenPGP signing key

%1

expired less than a day ago.

"); } else if (isOwnKey) { msg = ki18n("

Your OpenPGP encryption key

%1

expired less than a day ago.

"); } else { msg = ki18n("

The OpenPGP key for

%1

expired less than a day ago.

"); } return msg.subs(keyInfo).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np("

Your OpenPGP signing key

%2

expired yesterday.

", "

Your OpenPGP signing key

%2

expired %1 days ago.

"); } else if (isOwnKey) { msg = ki18np("

Your OpenPGP encryption key

%2

expired yesterday.

", "

Your OpenPGP encryption key

%2

expired %1 days ago.

"); } else { msg = ki18np("

The OpenPGP key for

%2

expired yesterday.

", "

The OpenPGP key for

%2

expired %1 days ago.

"); } return msg.subs(expiration.duration.count()).subs(keyInfo).toString(); } qCDebug(LIBKLEO_LOG) << "Key" << key << "expires in" << expiration.duration.count() << "days"; if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n("

Your OpenPGP signing key

%1

expires today.

"); } else if (isOwnKey) { msg = ki18n("

Your OpenPGP encryption key

%1

expires today.

"); } else { msg = ki18n("

The OpenPGP key for

%1

expires today.

"); } return msg.subs(keyInfo).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np("

Your OpenPGP signing key

%2

expires tomorrow.

", "

Your OpenPGP signing key

%2

expires in %1 days.

"); } else if (isOwnKey) { msg = ki18np("

Your OpenPGP encryption key

%2

expires tomorrow.

", "

Your OpenPGP encryption key

%2

expires in %1 days.

"); } else { msg = ki18np("

The OpenPGP key for

%2

expires tomorrow.

", "

The OpenPGP key for

%2

expires in %1 days.

"); } return msg.subs(expiration.duration.count()).subs(keyInfo).toString(); } QString formatSMIMEMessage(const GpgME::Key &orig_key, ExpiryChecker::Expiration expiration, ExpiryChecker::CheckFlags flags, bool ca) { const GpgME::Key key = expiration.certificate; const bool isOwnKey = flags & ExpiryChecker::OwnKey; const bool isSigningKey = flags & ExpiryChecker::SigningKey; const auto userCert = orig_key.isNull() ? key : orig_key; const auto userCertInfo = ki18nc("User ID of certificate (serial number serial no. of certificate)", "%1 (serial number %2)") .subs(Kleo::DN(userCert.userID(0).id()).prettyDN()) .subs(QString::fromLatin1(userCert.issuerSerial())); if (expiration.status == ExpiryChecker::Expired) { qCDebug(LIBKLEO_LOG) << "Certificate" << key << "expired" << expiration.duration.count() << "days ago"; if (ca) { if (key.isRoot()) { if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n( "

The root certificate

%2

" "

for your S/MIME signing certificate

%1

" "

expired less than a day ago.

"); } else if (isOwnKey) { msg = ki18n( "

The root certificate

%2

" "

for your S/MIME encryption certificate

%1

" "

expired less than a day ago.

"); } else { msg = ki18n( "

The root certificate

%2

" "

for S/MIME certificate

%1

" "

expired less than a day ago.

"); } return msg.subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np( "

The root certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expired yesterday.

", "

The root certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expired %1 days ago.

"); } else if (isOwnKey) { msg = ki18np( "

The root certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expired yesterday.

", "

The root certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expired %1 days ago.

"); } else { msg = ki18np( "

The root certificate

%3

" "

for S/MIME certificate

%2

" "

expired yesterday.

", "

The root certificate

%3

" "

for S/MIME certificate

%2

" "

expired %1 days ago.

"); } return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); } else { if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n( "

The intermediate CA certificate

%2

" "

for your S/MIME signing certificate

%1

" "

expired less than a day ago.

"); } else if (isOwnKey) { msg = ki18n( "

The intermediate CA certificate

%2

" "

for your S/MIME encryption certificate

%1

" "

expired less than a day ago.

"); } else { msg = ki18n( "

The intermediate CA certificate

%2

" "

for S/MIME certificate

%1

" "

expired less than a day ago.

"); } return msg.subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np( "

The intermediate CA certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expired yesterday.

", "

The intermediate CA certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expired %1 days ago.

"); } else if (isOwnKey) { msg = ki18np( "

The intermediate CA certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expired yesterday.

", "

The intermediate CA certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expired %1 days ago.

"); } else { msg = ki18np( "

The intermediate CA certificate

%3

" "

for S/MIME certificate

%2

" "

expired yesterday.

", "

The intermediate CA certificate

%3

" "

for S/MIME certificate

%2

" "

expired %1 days ago.

"); } return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); } } else { if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n("

Your S/MIME signing certificate

%1

expired less than a day ago.

"); } else if (isOwnKey) { msg = ki18n("

Your S/MIME encryption certificate

%1

expired less than a day ago.

"); } else { msg = ki18n("

The S/MIME certificate for

%1

expired less than a day ago.

"); } return msg.subs(userCertInfo).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np("

Your S/MIME signing certificate

%2

expired yesterday.

", "

Your S/MIME signing certificate

%2

expired %1 days ago.

"); } else if (isOwnKey) { msg = ki18np("

Your S/MIME encryption certificate

%2

expired yesterday.

", "

Your S/MIME encryption certificate

%2

expired %1 days ago.

"); } else { msg = ki18np("

The S/MIME certificate for

%2

expired yesterday.

", "

The S/MIME certificate for

%2

expired %1 days ago.

"); } return msg.subs(expiration.duration.count()).subs(userCertInfo).toString(); } } qCDebug(LIBKLEO_LOG) << "Certificate" << key << "expires in" << expiration.duration.count() << "days"; if (ca) { if (key.isRoot()) { if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n( "

The root certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expires today.

"); } else if (isOwnKey) { msg = ki18n( "

The root certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expires today.

"); } else { msg = ki18n( "

The root certificate

%3

" "

for S/MIME certificate

%2

" "

expires today.

"); } return msg.subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np( "

The root certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expires tomorrow.

", "

The root certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expires in %1 days.

"); } else if (isOwnKey) { msg = ki18np( "

The root certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expires tomorrow.

", "

The root certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expires in %1 days.

"); } else { msg = ki18np( "

The root certificate

%3

" "

for S/MIME certificate

%2

" "

expires tomorrow.

", "

The root certificate

%3

" "

for S/MIME certificate

%2

" "

expires in %1 days.

"); } return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); } if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n( "

The intermediate CA certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expires today.

"); } else if (isOwnKey) { msg = ki18n( "

The intermediate CA certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expires today.

"); } else { msg = ki18n( "

The intermediate CA certificate

%3

" "

for S/MIME certificate

%2

" "

expires today.

"); } } KLocalizedString msg; if (isSigningKey) { msg = ki18np( "

The intermediate CA certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expires tomorrow.

", "

The intermediate CA certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expires in %1 days.

"); } else if (isOwnKey) { msg = ki18np( "

The intermediate CA certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expires tomorrow.

", "

The intermediate CA certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expires in %1 days.

"); } else { msg = ki18np( "

The intermediate CA certificate

%3

" "

for S/MIME certificate

%2

" "

expires tomorrow.

", "

The intermediate CA certificate

%3

" "

for S/MIME certificate

%2

" "

expires in %1 days.

"); } return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); } if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n("

Your S/MIME signing certificate

%2

expires today.

"); } else if (isOwnKey) { msg = ki18n("

Your S/MIME encryption certificate

%2

expires today.

"); } else { msg = ki18n("

The S/MIME certificate for

%2

expires today.

"); } return msg.subs(userCertInfo).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np( "

Your S/MIME signing certificate

%2

" "

expires tomorrow.

", "

Your S/MIME signing certificate

%2

" "

expires in %1 days.

"); } else if (isOwnKey) { msg = ki18np( "

Your S/MIME encryption certificate

%2

" "

expires tomorrow.

", "

Your S/MIME encryption certificate

%2

" "

expires in %1 days.

"); } else { msg = ki18np( "

The S/MIME certificate for

%2

" "

expires tomorrow.

", "

The S/MIME certificate for

%2

" "

expires in %1 days.

"); } return msg.subs(expiration.duration.count()).subs(userCertInfo).toString(); } static GpgME::Subkey findBestSubkey(const GpgME::Key &key, ExpiryChecker::CheckFlags usageFlags) { // find the subkey with the latest expiration date for the given usage flags if (!(usageFlags & ExpiryChecker::UsageMask)) { // return primary key if no specific usage is specified (as for chain certificates) return key.subkey(0); } GpgME::Subkey result; for (unsigned int i = 0; i < key.numSubkeys(); ++i) { const auto subkey = key.subkey(i); if (subkey.isRevoked() || subkey.isInvalid() || subkey.isDisabled()) { // unusable subkey continue; } if (((usageFlags & ExpiryChecker::EncryptionKey) && !subkey.canEncrypt()) // || ((usageFlags & ExpiryChecker::SigningKey) && !subkey.canSign()) // || ((usageFlags & ExpiryChecker::CertificationKey) && !subkey.canCertify())) { // unsuitable subkey for requested usage continue; } if (subkey.neverExpires()) { - // stop looking for the best subkey if we found a suitable subkey that doesn't expire - return subkey; + // stop looking for the best subkey if we found a suitable subkey that doesn't expire; + // return the primary key because a non-expiring subkey inherits the primary key's expiration + return key.subkey(0); } if (quint32(subkey.expirationTime()) > quint32(result.expirationTime())) { result = subkey; } } return result; } ExpiryChecker::Expiration ExpiryCheckerPrivate::calculateExpiration(const GpgME::Subkey &subkey) const { if (subkey.neverExpires()) { return {subkey.parent(), ExpiryChecker::NotNearExpiry, Kleo::chrono::days::zero()}; } const time_t currentTime = timeProvider ? timeProvider->currentTime() : std::time(nullptr); const auto currentDate = timeProvider ? timeProvider->currentDate() : QDate::currentDate(); const auto timeSpec = timeProvider ? timeProvider->timeSpec() : Qt::LocalTime; const time_t expirationTime = subkey.expirationTime(); const auto expirationDate = QDateTime::fromSecsSinceEpoch(quint32(expirationTime), timeSpec).date(); // use std::difftime to avoid problems with negative values on 32-bit systems if (std::difftime(expirationTime, currentTime) <= 0) { return {subkey.parent(), ExpiryChecker::Expired, Kleo::chrono::days{expirationDate.daysTo(currentDate)}}; } else { return {subkey.parent(), ExpiryChecker::ExpiresSoon, Kleo::chrono::days{currentDate.daysTo(expirationDate)}}; } } ExpiryChecker::Expiration ExpiryCheckerPrivate::checkForExpiration(const GpgME::Key &key, // Kleo::chrono::days threshold, ExpiryChecker::CheckFlags usageFlags) const { const auto subkey = findBestSubkey(key, usageFlags); if (subkey.isNull()) { return {key, ExpiryChecker::NoSuitableSubkey, {}}; } ExpiryChecker::Expiration expiration = calculateExpiration(subkey); if ((expiration.status == ExpiryChecker::ExpiresSoon) && (expiration.duration > threshold)) { // key expires, but not too soon expiration.status = ExpiryChecker::NotNearExpiry; } return expiration; } ExpiryChecker::Result ExpiryCheckerPrivate::checkKeyNearExpiry(const GpgME::Key &orig_key, ExpiryChecker::CheckFlags flags) { static const int maximumCertificateChainLength = 100; const bool isOwnKey = flags & ExpiryChecker::OwnKey; ExpiryChecker::Result result; result.checkFlags = flags; result.expiration.certificate = orig_key; // use vector instead of set because certificate chains are usually very short std::vector checkedCertificates; auto key = orig_key; for (int chainCount = 0; chainCount < maximumCertificateChainLength; ++chainCount) { checkedCertificates.push_back(key.primaryFingerprint()); const GpgME::Subkey subkey = key.subkey(0); const bool newMessage = !alreadyWarnedFingerprints.count(subkey.fingerprint()); const auto threshold = chainCount > 0 // ? (key.isRoot() ? settings.rootCertThreshold() : settings.chainCertThreshold()) // : (isOwnKey ? settings.ownKeyThreshold() : settings.otherKeyThreshold()); const auto usageFlags = (chainCount == 0) ? (flags & ExpiryChecker::UsageMask) : ExpiryChecker::CheckFlags{}; const auto expiration = checkForExpiration(key, threshold, usageFlags); if (chainCount == 0) { result.expiration = expiration; } else if (expiration.status != ExpiryChecker::NotNearExpiry) { result.chainExpiration.push_back(expiration); } if (expiration.status == ExpiryChecker::Expired) { const QString msg = key.protocol() == GpgME::OpenPGP // ? formatOpenPGPMessage(expiration, flags) : formatSMIMEMessage(orig_key, expiration, flags, chainCount > 0); alreadyWarnedFingerprints.insert(subkey.fingerprint()); Q_EMIT q->expiryMessage(key, msg, isOwnKey ? ExpiryChecker::OwnKeyExpired : ExpiryChecker::OtherKeyExpired, newMessage); } else if (expiration.status == ExpiryChecker::ExpiresSoon) { const QString msg = key.protocol() == GpgME::OpenPGP // ? formatOpenPGPMessage(expiration, flags) : formatSMIMEMessage(orig_key, expiration, flags, chainCount > 0); alreadyWarnedFingerprints.insert(subkey.fingerprint()); Q_EMIT q->expiryMessage(key, msg, isOwnKey ? ExpiryChecker::OwnKeyNearExpiry : ExpiryChecker::OtherKeyNearExpiry, newMessage); } else if (expiration.status == ExpiryChecker::NoSuitableSubkey) { break; } if (!(flags & ExpiryChecker::CheckChain) || key.isRoot() || (key.protocol() != GpgME::CMS)) { break; } const auto keys = KeyCache::instance()->findIssuers(key, KeyCache::NoOption); if (keys.empty()) { break; } key = keys.front(); if (Kleo::contains(checkedCertificates, key.primaryFingerprint())) { break; // this certificate was already checked (looks like a circle in the chain) } } return result; } ExpiryChecker::Result ExpiryChecker::checkKey(const GpgME::Key &key, CheckFlags flags) const { if (key.isNull()) { qWarning(LIBKLEO_LOG) << __func__ << "called with null key"; return {flags, {key, InvalidKey, {}}, {}}; } if (!(flags & UsageMask)) { qWarning(LIBKLEO_LOG) << __func__ << "called with invalid flags:" << flags; return {flags, {key, InvalidCheckFlags, {}}, {}}; } return d->checkKeyNearExpiry(key, flags); } void ExpiryChecker::setTimeProviderForTest(const std::shared_ptr &timeProvider) { d->timeProvider = timeProvider; }