diff --git a/src/kleo/expirychecker.cpp b/src/kleo/expirychecker.cpp index 22486492e..02e0df6df 100644 --- a/src/kleo/expirychecker.cpp +++ b/src/kleo/expirychecker.cpp @@ -1,493 +1,493 @@ /* 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, 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(); } 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 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 {key, ExpiryChecker::Expired, Kleo::chrono::days{expirationDate.daysTo(currentDate)}}; } else { - return {key, ExpiryChecker::Expires, Kleo::chrono::days{currentDate.daysTo(expirationDate)}}; + return {key, ExpiryChecker::ExpiresSoon, Kleo::chrono::days{currentDate.daysTo(expirationDate)}}; } } 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)) { + 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 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 f6a4232b1..ffd3e17b2 100644 --- a/src/kleo/expirychecker.h +++ b/src/kleo/expirychecker.h @@ -1,114 +1,113 @@ /* 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 #include namespace Kleo { class ExpiryCheckerPrivate; class ExpiryCheckerSettings; class KLEO_EXPORT TimeProvider { public: virtual ~TimeProvider() = default; virtual time_t currentTime() const = 0; virtual QDate currentDate() const = 0; virtual Qt::TimeSpec timeSpec() const = 0; }; class KLEO_EXPORT ExpiryChecker : public QObject { Q_OBJECT public: enum CheckFlag { EncryptionKey = 0x01, SigningKey = 0x02, OwnKey = 0x04, OwnEncryptionKey = OwnKey | EncryptionKey, OwnSigningKey = OwnKey | SigningKey, CheckChain = 0x08, }; 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 days until expiry if status is ExpiresSoon (i.e. 0 // if expiry is today, 1 if it's tomorrow, etc.), // days since expiry if status is Expired, and // 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, 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)