diff --git a/src/kleo/expirychecker.cpp b/src/kleo/expirychecker.cpp index 781751b22..2ce9088da 100644 --- a/src/kleo/expirychecker.cpp +++ b/src/kleo/expirychecker.cpp @@ -1,452 +1,453 @@ /* 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::Key &key) const; ExpiryChecker::Expiration checkForExpiration(const GpgME::Key &key, Kleo::chrono::days threshold) 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) - : d{new ExpiryCheckerPrivate{this, settings}} +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 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 &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 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 one 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 one 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 one 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 one day ago.

", "

Your S/MIME signing certificate

%2

" "

expired %1 days ago.

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

Your S/MIME encryption certificate

%2

" "

expired one day ago.

", "

Your S/MIME encryption certificate

%2

" "

expired %1 days ago.

"); } else { msg = ki18np( "

The S/MIME certificate for

%2

" "

expired one 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(); } ExpiryChecker::Expiration ExpiryCheckerPrivate::calculateExpiration(const GpgME::Key &key) const { const GpgME::Subkey subkey = key.subkey(0); if (subkey.neverExpires()) { return {key, ExpiryChecker::NotNearExpiry, 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 {key, secsTillExpiry <= 0 ? ExpiryChecker::Expired : ExpiryChecker::Expires, std::chrono::duration_cast(std::chrono::seconds{std::abs(secsTillExpiry)})}; } ExpiryChecker::Expiration ExpiryCheckerPrivate::checkForExpiration(const GpgME::Key &key, Kleo::chrono::days threshold) const { ExpiryChecker::Expiration expiration = calculateExpiration(key); if ((expiration.status == ExpiryChecker::Expires) && (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 expiration = checkForExpiration(key, threshold); 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); } 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 { return d->checkKeyNearExpiry(key, flags); } void ExpiryChecker::setTimeProviderForTest(const std::shared_ptr &timeProvider) { d->timeProvider = timeProvider; } diff --git a/src/kleo/expirychecker.h b/src/kleo/expirychecker.h index 1e1680428..73d6bfda3 100644 --- a/src/kleo/expirychecker.h +++ b/src/kleo/expirychecker.h @@ -1,110 +1,110 @@ /* 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 */ #pragma once #include "kleo_export.h" #include #include #include #include namespace Kleo { class ExpiryCheckerPrivate; class ExpiryCheckerSettings; class KLEO_EXPORT TimeProvider { public: virtual ~TimeProvider() = default; virtual time_t getTime() const = 0; }; class KLEO_EXPORT ExpiryChecker : public QObject { Q_OBJECT public: enum CheckFlag { NoCheckFlags = 0, OwnKey = 1, OwnEncryptionKey = OwnKey, SigningKey = 2, OwnSigningKey = OwnKey | SigningKey, CheckChain = 4, }; Q_FLAG(CheckFlag) Q_DECLARE_FLAGS(CheckFlags, CheckFlag) Q_FLAG(CheckFlags) enum ExpirationStatus { NotNearExpiry, ExpiresSoon, Expired, Expires = ExpiresSoon, // alias used internally }; Q_ENUM(ExpirationStatus) struct Expiration { GpgME::Key certificate; ExpirationStatus status; // duration is full days until expiry if status is Expires, // full days since expiry if status is Expired, // undefined if status is NotNearExpiry Kleo::chrono::days duration; }; struct Result { CheckFlags checkFlags; Expiration expiration; // result for the checked certificate std::vector chainExpiration; // results for expired or soon expiring chain certificates }; - explicit ExpiryChecker(const ExpiryCheckerSettings &settings); + explicit ExpiryChecker(const ExpiryCheckerSettings &settings, QObject *parent = nullptr); ~ExpiryChecker() override; Q_REQUIRED_RESULT ExpiryCheckerSettings settings() const; enum ExpiryInformation { OwnKeyExpired, OwnKeyNearExpiry, OtherKeyExpired, OtherKeyNearExpiry, }; Q_ENUM(ExpiryInformation) Result checkKey(const GpgME::Key &key, CheckFlags flags) const; Q_SIGNALS: void expiryMessage(const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) const; public: void setTimeProviderForTest(const std::shared_ptr &); private: std::unique_ptr const d; }; Q_DECLARE_OPERATORS_FOR_FLAGS(ExpiryChecker::CheckFlags) } Q_DECLARE_METATYPE(Kleo::ExpiryChecker::CheckFlags) Q_DECLARE_METATYPE(Kleo::ExpiryChecker::ExpiryInformation) Q_DECLARE_METATYPE(GpgME::Key)