diff --git a/src/kleo/expirychecker.cpp b/src/kleo/expirychecker.cpp index 4d7638553..f74792885 100644 --- a/src/kleo/expirychecker.cpp +++ b/src/kleo/expirychecker.cpp @@ -1,443 +1,441 @@ /* 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 using namespace Kleo; namespace { struct Expiration { enum Status { NeverExpires, Expires, Expired, } status; // duration is full days until expiry if status is Expires, // full days since expiry if status is Expired, // undefined if status is NeverExpires Kleo::chrono::days duration; }; } class Kleo::ExpiryCheckerPrivate { Kleo::ExpiryChecker *q; public: ExpiryCheckerPrivate(ExpiryChecker *qq, const ExpiryCheckerSettings &settings_) : q{qq} , settings{settings_} { } Expiration calculateExpiration(const GpgME::Subkey &key) const; - void checkKeyNearExpiry(const GpgME::Key &key, - ExpiryChecker::KeyFlags flags, - bool ca = false, - int recur_limit = 100, - const GpgME::Key &orig_key = GpgME::Key::null); + void checkKeyNearExpiry(const GpgME::Key &key, ExpiryChecker::KeyFlags flags); ExpiryCheckerSettings settings; std::set alreadyWarnedFingerprints; std::shared_ptr timeProvider; }; ExpiryChecker::ExpiryChecker(const ExpiryCheckerSettings &settings) : d{new ExpiryCheckerPrivate{this, settings}} { } ExpiryChecker::~ExpiryChecker() = default; ExpiryCheckerSettings ExpiryChecker::settings() const { return d->settings; } QString formatOpenPGPMessage(const GpgME::Key &key, Expiration expiration, ExpiryChecker::KeyFlags flags) { 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 == Expiration::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 one day ago.

", "

Your OpenPGP signing key

%2

" "

expired %1 days ago.

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

Your OpenPGP encryption key

%2

" "

expired one day ago.

", "

Your OpenPGP encryption key

%2

" "

expired %1 days ago.

"); } else { msg = ki18np( "

The OpenPGP key for

%2

" "

expired one day ago.

", "

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 less than" << expiration.duration.count() + 1 << "days"; KLocalizedString msg; if (isSigningKey) { msg = ki18np( "

Your OpenPGP signing key

%2

" "

expires in less than a day.

", "

Your OpenPGP signing key

%2

" "

expires in less than %1 days.

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

Your OpenPGP encryption key

%2

" "

expires in less than a day.

", "

Your OpenPGP encryption key

%2

" "

expires in less than %1 days.

"); } else { msg = ki18np( "

The OpenPGP key for

%2

" "

expires in less than a day.

", "

The OpenPGP key for

%2

" "

expires in less than %1 days.

"); } return msg.subs(expiration.duration.count() + 1).subs(keyInfo).toString(); } QString formatSMIMEMessage(const GpgME::Key &key, const GpgME::Key &orig_key, Expiration expiration, ExpiryChecker::KeyFlags flags, bool ca) { 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 == Expiration::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 one day ago.

", "

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 one day ago.

", "

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 one day ago.

", "

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 less than a day ago.

", "

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 less than a day ago.

", "

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 less than a day ago.

", "

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 less than a day ago.

", "

Your S/MIME signing certificate

%2

" "

expired %1 days ago.

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

Your S/MIME encryption certificate

%2

" "

expired less than a day ago.

", "

Your S/MIME encryption certificate

%2

" "

expired %1 days ago.

"); } else { msg = ki18np( "

The S/MIME certificate for

%2

" "

expired less than a day ago.

", "

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 less than" << expiration.duration.count() + 1 << "days"; KLocalizedString msg; if (ca) { if (key.isRoot()) { if (isSigningKey) { msg = ki18np( "

The root certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expires in less than a day.

", "

The root certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expires in less than %1 days.

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

The root certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expires in less than a day.

", "

The root certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expires in less than %1 days.

"); } else { msg = ki18np( "

The root certificate

%3

" "

for S/MIME certificate

%2

" "

expires in less than a day.

", "

The root certificate

%3

" "

for S/MIME certificate

%2

" "

expires in less than %1 days.

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

The intermediate CA certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expires in less than a day.

", "

The intermediate CA certificate

%3

" "

for your S/MIME signing certificate

%2

" "

expires in less than %1 days.

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

The intermediate CA certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expires in less than a day.

", "

The intermediate CA certificate

%3

" "

for your S/MIME encryption certificate

%2

" "

expires in less than %1 days.

"); } else { msg = ki18np( "

The intermediate CA certificate

%3

" "

for S/MIME certificate

%2

" "

expires in less than a day.

", "

The intermediate CA certificate

%3

" "

for S/MIME certificate

%2

" "

expires in less than %1 days.

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

Your S/MIME signing certificate

%2

" "

expires in less than a day.

", "

Your S/MIME signing certificate

%2

" "

expires in less than %1 days.

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

Your S/MIME encryption certificate

%2

" "

expires in less than a day.

", "

Your S/MIME encryption certificate

%2

" "

expires in less than %1 days.

"); } else { msg = ki18np( "

The S/MIME certificate for

%2

" "

expires in less than a day.

", "

The S/MIME certificate for

%2

" "

expires in less than %1 days.

"); } return msg.subs(expiration.duration.count() + 1).subs(userCertInfo).toString(); } Expiration ExpiryCheckerPrivate::calculateExpiration(const GpgME::Subkey &subkey) const { if (subkey.neverExpires()) { return {Expiration::NeverExpires, Kleo::chrono::days::zero()}; } const time_t t = timeProvider ? timeProvider->getTime() : std::time(nullptr); // casting the double-valued difference (returned by std::difftime) of two non-negative time_t to a time_t is no problem; // negative values for expiration time and current time can be safely ignored const time_t secsTillExpiry = static_cast(std::difftime(subkey.expirationTime(), t)); return {secsTillExpiry <= 0 ? Expiration::Expired : Expiration::Expires, std::chrono::duration_cast(std::chrono::seconds{std::abs(secsTillExpiry)})}; } -void ExpiryCheckerPrivate::checkKeyNearExpiry(const GpgME::Key &key, ExpiryChecker::KeyFlags flags, bool ca, int recur_limit, const GpgME::Key &orig_key) +void ExpiryCheckerPrivate::checkKeyNearExpiry(const GpgME::Key &orig_key, ExpiryChecker::KeyFlags flags) { + static const int maximumCertificateChainLength = 100; const bool isOwnKey = flags & ExpiryChecker::OwnKey; - if (recur_limit <= 0) { - qCDebug(LIBKLEO_LOG) << "Key chain too long (>100 certs)"; - return; - } - const GpgME::Subkey subkey = key.subkey(0); - const bool newMessage = !alreadyWarnedFingerprints.count(subkey.fingerprint()); + auto key = orig_key; + for (int chainCount = 0; chainCount < maximumCertificateChainLength; ++chainCount) { + const GpgME::Subkey subkey = key.subkey(0); - const auto expiration = calculateExpiration(subkey); - if (expiration.status == Expiration::NeverExpires) { - return; - } - if (expiration.status == Expiration::Expired) { - const QString msg = key.protocol() == GpgME::OpenPGP // - ? formatOpenPGPMessage(key, expiration, flags) - : formatSMIMEMessage(key, orig_key, expiration, flags, ca); - alreadyWarnedFingerprints.insert(subkey.fingerprint()); - Q_EMIT q->expiryMessage(key, msg, isOwnKey ? ExpiryChecker::OwnKeyExpired : ExpiryChecker::OtherKeyExpired, newMessage); - } else { - const auto threshold = ca // - ? (key.isRoot() ? settings.rootCertThreshold() : settings.chainCertThreshold()) // - : (isOwnKey ? settings.ownKeyThreshold() : settings.otherKeyThreshold()); - if (threshold >= Kleo::chrono::days::zero() && expiration.duration <= threshold) { + const bool newMessage = !alreadyWarnedFingerprints.count(subkey.fingerprint()); + + const auto expiration = calculateExpiration(subkey); + if (expiration.status == Expiration::NeverExpires) { + break; + } + if (expiration.status == Expiration::Expired) { const QString msg = key.protocol() == GpgME::OpenPGP // ? formatOpenPGPMessage(key, expiration, flags) - : formatSMIMEMessage(key, orig_key, expiration, flags, ca); + : formatSMIMEMessage(key, orig_key, expiration, flags, chainCount > 0); alreadyWarnedFingerprints.insert(subkey.fingerprint()); - Q_EMIT q->expiryMessage(key, msg, isOwnKey ? ExpiryChecker::OwnKeyNearExpiry : ExpiryChecker::OtherKeyNearExpiry, newMessage); + Q_EMIT q->expiryMessage(key, msg, isOwnKey ? ExpiryChecker::OwnKeyExpired : ExpiryChecker::OtherKeyExpired, newMessage); + } else { + const auto threshold = chainCount > 0 // + ? (key.isRoot() ? settings.rootCertThreshold() : settings.chainCertThreshold()) // + : (isOwnKey ? settings.ownKeyThreshold() : settings.otherKeyThreshold()); + if (threshold >= Kleo::chrono::days::zero() && expiration.duration <= threshold) { + const QString msg = key.protocol() == GpgME::OpenPGP // + ? formatOpenPGPMessage(key, expiration, flags) + : formatSMIMEMessage(key, orig_key, expiration, flags, chainCount > 0); + alreadyWarnedFingerprints.insert(subkey.fingerprint()); + Q_EMIT q->expiryMessage(key, msg, isOwnKey ? ExpiryChecker::OwnKeyNearExpiry : ExpiryChecker::OtherKeyNearExpiry, newMessage); + } } - } - if (key.isRoot()) { - return; - } else if (key.protocol() != GpgME::CMS) { // Key chaining is only possible in SMIME - return; - } - const auto keys = KeyCache::instance()->findIssuers(key, KeyCache::NoOption); - if (!keys.empty()) { - return checkKeyNearExpiry(keys.front(), flags, true, recur_limit - 1, ca ? orig_key : key); + if (key.isRoot()) { + break; + } else if (key.protocol() != GpgME::CMS) { // Key chaining is only possible in SMIME + break; + } + const auto keys = KeyCache::instance()->findIssuers(key, KeyCache::NoOption); + if (keys.empty()) { + break; + } + key = keys.front(); } } void ExpiryChecker::checkKey(const GpgME::Key &key, KeyFlags flags) const { d->checkKeyNearExpiry(key, flags); } void ExpiryChecker::setTimeProviderForTest(const std::shared_ptr &timeProvider) { d->timeProvider = timeProvider; }