diff --git a/CMakeLists.txt b/CMakeLists.txt index b9d031f42..eea833988 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ option(WITH_LIBDNF5_CLI "Build library for working with a terminal in a command- option(WITH_DNF5 "Build dnf5 command-line package manager" ON) option(WITH_DNF5_PLUGINS "Build plugins for dnf5 command-line package manager" ON) option(WITH_PLUGIN_ACTIONS "Build a dnf5 actions plugin" ON) +option(WITH_PLUGIN_EXPIRED_PGP_KEYS "Build a libdnf5 expired pgp keys plugin" ON) option(WITH_PLUGIN_RHSM "Build a libdnf5 rhsm (Red Hat Subscription Manager) plugin" OFF) option(WITH_PYTHON_PLUGINS_LOADER "Build a special dnf5 plugin that loads Python plugins. Requires WITH_PYTHON3=ON." ON) diff --git a/dnf5.spec b/dnf5.spec index 3d9eae939..cd0cb8097 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -77,6 +77,7 @@ Provides: dnf5-command(versionlock) %bcond_without dnf5 %bcond_without dnf5_plugins %bcond_without plugin_actions +%bcond_without plugin_expired_pgp_keys %bcond_without plugin_rhsm %bcond_without python_plugins_loader @@ -585,6 +586,24 @@ Libdnf5 plugin that allows to run actions (external executables) on hooks. %endif +# ========== libdnf5-plugin-expired-pgp-keys ========== + +%if %{with plugin_expired_pgp_keys} +%package -n libdnf5-plugin-expired-pgp-keys +Summary: Libdnf5 plugin for detecting and removing expired PGP keys +License: LGPL-2.1-or-later +Requires: libdnf5%{?_isa} = %{version}-%{release} + +%description -n libdnf5-plugin-expired-pgp-keys +Libdnf5 plugin for detecting and removing expired PGP keys. + +%files -n libdnf5-plugin-expired-pgp-keys -f libdnf5-plugin-expired-pgp-keys.lang +%{_libdir}/libdnf5/plugins/expired-pgp-keys.* +%config %{_sysconfdir}/dnf/libdnf5-plugins/expired-pgp-keys.conf +%{_mandir}/man8/libdnf5-expired-pgp-keys.8.* +%endif + + # ========== libdnf5-plugin-plugin_rhsm ========== %if %{with plugin_rhsm} @@ -774,6 +793,7 @@ automatically and regularly from systemd timers, cron jobs or similar. -DWITH_LIBDNF5_CLI=%{?with_libdnf_cli:ON}%{!?with_libdnf_cli:OFF} \ -DWITH_DNF5=%{?with_dnf5:ON}%{!?with_dnf5:OFF} \ -DWITH_PLUGIN_ACTIONS=%{?with_plugin_actions:ON}%{!?with_plugin_actions:OFF} \ + -DWITH_PLUGIN_EXPIRED_PGP_KEYS=%{?with_plugin_expired_pgp_keys:ON}%{!?with_plugin_expired_pgp_keys:OFF} \ -DWITH_PLUGIN_RHSM=%{?with_plugin_rhsm:ON}%{!?with_plugin_rhsm:OFF} \ -DWITH_PYTHON_PLUGINS_LOADER=%{?with_python_plugins_loader:ON}%{!?with_python_plugins_loader:OFF} \ \ @@ -864,6 +884,7 @@ popd %find_lang libdnf5 %find_lang libdnf5-cli %find_lang libdnf5-plugin-actions +%find_lang libdnf5-plugin-expired-pgp-keys %find_lang libdnf5-plugin-rhsm %ldconfig_scriptlets diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 27052df06..76118f7fc 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -41,6 +41,10 @@ if(WITH_MAN) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/libdnf5-actions.8 DESTINATION share/man/man8) endif() + if(WITH_PLUGIN_EXPIRED_PGP_KEYS) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/libdnf5-expired-pgp-keys.8 DESTINATION share/man/man8) + endif() + if(WITH_DNF5DAEMON_CLIENT) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5daemon-client.8 DESTINATION share/man/man8) endif() diff --git a/doc/conf.py.in b/doc/conf.py.in index f2d86d80b..0ce662590 100644 --- a/doc/conf.py.in +++ b/doc/conf.py.in @@ -150,6 +150,7 @@ man_pages = [ ('dnf5_plugins/needs_restarting.8', 'dnf5-needs-restarting', 'Needs-restarting Command', AUTHORS, 8), ('dnf5_plugins/repoclosure.8', 'dnf5-repoclosure', 'Repoclosure Command', AUTHORS, 8), ('libdnf5_plugins/actions.8', 'libdnf5-actions', 'Actions Plugin', AUTHORS, 8), + ('libdnf5_plugins/expired-pgp-keys.8', 'libdnf5-expired-pgp-keys', 'Expired PGP Keys Plugin', AUTHORS, 8), ('misc/aliases.7', 'dnf5-aliases', 'Aliases for command line arguments', AUTHORS, 7), ('misc/caching.7', 'dnf5-caching', 'Caching', AUTHORS, 7), ('misc/comps.7', 'dnf5-comps', 'Comps Groups And Environments', AUTHORS, 7), diff --git a/doc/dnf5.8.rst b/doc/dnf5.8.rst index d49f38ecf..216e556c0 100644 --- a/doc/dnf5.8.rst +++ b/doc/dnf5.8.rst @@ -452,6 +452,7 @@ Application Plugins: Library Plugins: | :manpage:`libdnf5-actions(8)`, :ref:`Actions plugin ` + | :manpage:`libdnf5-expired-pgp-keys(8)`, :ref:`Expired PGP keys plugin ` Configuration: | :manpage:`dnf5-conf(5)`, :ref:`DNF5 Configuration Reference ` diff --git a/doc/libdnf5_plugins/expired-pgp-keys.8.rst b/doc/libdnf5_plugins/expired-pgp-keys.8.rst new file mode 100644 index 000000000..fefc1b21e --- /dev/null +++ b/doc/libdnf5_plugins/expired-pgp-keys.8.rst @@ -0,0 +1,40 @@ +.. + Copyright Contributors to the libdnf project. + + This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + + Libdnf is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Libdnf is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libdnf. If not, see . + +.. _expired-pgp-keys_plugin_ref-label: + +######################## + Expired PGP Keys Plugin +######################## + +Description +=========== + +The plugin checks for installed but expired PGP keys before executing the transaction. +For each expired key, the user is prompted with information about the specific key +and can confirm its removal, allowing for the import of an updated key later. +When the ``assumeyes`` option is configured, expired keys are removed automatically. + +Configuration +============= + +The plugin configuration is in ``/etc/dnf/libdnf5-plugins/expired-pgp-keys.conf``. All configuration +options are in the ``[main]`` section. + +``enabled`` + Whether the plugin is enabled. Default value is ``False``. diff --git a/doc/libdnf5_plugins/index.rst b/doc/libdnf5_plugins/index.rst index 9e02d158f..264f39a25 100644 --- a/doc/libdnf5_plugins/index.rst +++ b/doc/libdnf5_plugins/index.rst @@ -9,5 +9,6 @@ LIBDNF5 Plugins :maxdepth: 1 actions.8 + expired-pgp-keys.8 .. diff --git a/libdnf5-plugins/CMakeLists.txt b/libdnf5-plugins/CMakeLists.txt index f97124f09..bf224126c 100644 --- a/libdnf5-plugins/CMakeLists.txt +++ b/libdnf5-plugins/CMakeLists.txt @@ -2,5 +2,6 @@ set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_C_VISIBILITY_PRESET hidden) add_subdirectory("actions") +add_subdirectory("expired-pgp-keys") add_subdirectory("python_plugins_loader") add_subdirectory("rhsm") diff --git a/libdnf5-plugins/expired-pgp-keys/CMakeLists.txt b/libdnf5-plugins/expired-pgp-keys/CMakeLists.txt new file mode 100644 index 000000000..7ff9d1152 --- /dev/null +++ b/libdnf5-plugins/expired-pgp-keys/CMakeLists.txt @@ -0,0 +1,24 @@ +if(NOT WITH_PLUGIN_EXPIRED_PGP_KEYS) + return() +endif() + +# set gettext domain for translations +set(GETTEXT_DOMAIN libdnf5-plugin-expired-pgp-keys) +add_definitions(-DGETTEXT_DOMAIN=\"${GETTEXT_DOMAIN}\") + +add_library(expired-pgp-keys MODULE expired-pgp-keys.cpp) + +# disable the 'lib' prefix in order to create expired-pgp-keys.so +set_target_properties(expired-pgp-keys PROPERTIES PREFIX "") + +target_link_libraries(expired-pgp-keys PRIVATE common) +target_link_libraries(expired-pgp-keys PRIVATE libdnf5 libdnf5-cli) + +pkg_check_modules(RPM REQUIRED rpm) +target_link_libraries(expired-pgp-keys PRIVATE ${RPM_LIBRARIES}) + +install(TARGETS expired-pgp-keys LIBRARY DESTINATION "${CMAKE_INSTALL_FULL_LIBDIR}/libdnf5/plugins/") + +install(FILES "expired-pgp-keys.conf" DESTINATION "${CMAKE_INSTALL_FULL_SYSCONFDIR}/dnf/libdnf5-plugins") + +add_subdirectory(po) diff --git a/libdnf5-plugins/expired-pgp-keys/expired-pgp-keys.conf b/libdnf5-plugins/expired-pgp-keys/expired-pgp-keys.conf new file mode 100644 index 000000000..a5ba3df9f --- /dev/null +++ b/libdnf5-plugins/expired-pgp-keys/expired-pgp-keys.conf @@ -0,0 +1,3 @@ +[main] +name = expired-pgp-keys +enabled = 0 diff --git a/libdnf5-plugins/expired-pgp-keys/expired-pgp-keys.cpp b/libdnf5-plugins/expired-pgp-keys/expired-pgp-keys.cpp new file mode 100644 index 000000000..fd0826ec2 --- /dev/null +++ b/libdnf5-plugins/expired-pgp-keys/expired-pgp-keys.cpp @@ -0,0 +1,235 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +Libdnf is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with libdnf. If not, see . +*/ + +#include "utils/string.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace libdnf5; + +namespace { + +constexpr const char * PLUGIN_NAME = "expired-pgp-keys"; +constexpr plugin::Version PLUGIN_VERSION{1, 0, 0}; + +constexpr const char * attrs[]{"author.name", "author.email", "description", nullptr}; +constexpr const char * attrs_value[]{"Jan Kolarik", "jkolarik@redhat.com", "Expired PGP Keys Plugin."}; + +/// @brief Find expired PGP keys and suggest their removal. +/// This is a workaround to solve https://github.com/rpm-software-management/dnf5/issues/1192. +class ExpiredPgpKeys final : public plugin::IPlugin { +public: + ExpiredPgpKeys(libdnf5::plugin::IPluginData & data, libdnf5::ConfigParser &) : IPlugin(data) {} + virtual ~ExpiredPgpKeys() = default; + + PluginAPIVersion get_api_version() const noexcept override { return PLUGIN_API_VERSION; } + + const char * get_name() const noexcept override { return PLUGIN_NAME; } + + plugin::Version get_version() const noexcept override { return PLUGIN_VERSION; } + + const char * const * get_attributes() const noexcept override { return attrs; } + + const char * get_attribute(const char * attribute) const noexcept override { + for (size_t i = 0; attrs[i]; ++i) { + if (std::strcmp(attribute, attrs[i]) == 0) { + return attrs_value[i]; + } + } + return nullptr; + } + + void pre_transaction(const libdnf5::base::Transaction & transaction) override { + process_expired_pgp_keys(transaction); + } + +private: + void process_expired_pgp_keys(const libdnf5::base::Transaction & transaction) const; +}; + +/// Check that GPG is installed to enable querying expired keys later. +static bool is_gpg_installed() { + auto ts = rpmtsCreate(); + rpmdbMatchIterator mi; + mi = rpmtsInitIterator(ts, RPMDBI_PROVIDENAME, "gpg", 0); + bool found = rpmdbNextIterator(mi) != NULL; + rpmdbFreeIterator(mi); + rpmtsFree(ts); + return found; +} + +/// Check if the transaction contains any inbound actions. +/// This determines if new software is to be installed, which might require downloading a new PGP signing key. +static bool any_inbound_action(const libdnf5::base::Transaction & transaction) { + for (const auto & package : transaction.get_transaction_packages()) { + if (transaction_item_action_is_inbound(package.get_action())) { + return true; + } + } + return false; +} + +/// Retrieve the PGP key expiration timestamp, or return -1 if the expiration is not available. +static int64_t get_key_expire_timestamp(Header hdr) { + // open gpg process to retrieve information about the key + const char * key = headerGetString(hdr, RPMTAG_DESCRIPTION); + std::unique_ptr pipe( + popen(libdnf5::utils::sformat("echo \"{}\" | gpg --show-keys --with-colon", key).c_str(), "r"), pclose); + if (!pipe) { + return -1; + } + + // read key information from the gpg process + char buffer[1024]; + std::string output; + while (fgets(buffer, sizeof(buffer), pipe.get()) != nullptr) { + output += buffer; + } + + // check expired time is a numeric value + auto expired_date_string = libdnf5::utils::string::split(libdnf5::utils::string::split(output, "\n")[0], ":")[6]; + if (expired_date_string.empty() || expired_date_string.find_first_not_of("0123456789") != std::string::npos) { + return -1; + } + + return std::stoull(expired_date_string); +} + +/// Returns a list of expired PGP keys, each represented as a tuple (`hdr`, `date`): +/// * `hdr`: An RPM header object representing the key. +/// * `date`: A `datetime` object indicating the key's expiration date. +static std::vector> list_expired_keys() { + std::vector> expired_keys; + + auto current_date = std::chrono::system_clock::now(); + auto current_timestamp = std::chrono::duration_cast(current_date.time_since_epoch()).count(); + + auto ts = rpmtsCreate(); + rpmdbMatchIterator mi; + mi = rpmtsInitIterator(ts, RPMDBI_NAME, "gpg-pubkey", 0); + Header hdr; + while ((hdr = rpmdbNextIterator(mi)) != nullptr) { + auto key_timestamp = get_key_expire_timestamp(hdr); + if (key_timestamp > 0 && key_timestamp < current_timestamp) { + expired_keys.push_back({hdr, key_timestamp}); + headerLink(hdr); + } + } + + rpmdbFreeIterator(mi); + rpmtsFree(ts); + + return expired_keys; +} + +/// Remove the system package corresponding to the PGP key from the given RPM header. +static bool remove_pgp_key(Header hdr) { + auto ts = rpmtsCreate(); + rpmtsAddEraseElement(ts, hdr, -1); + bool result = rpmtsRun(ts, nullptr, RPMPROB_FILTER_NONE) == 0; + rpmtsFree(ts); + return result; +} + +void ExpiredPgpKeys::process_expired_pgp_keys(const libdnf5::base::Transaction & transaction) const { + const auto & config = get_base().get_config(); + + if (!config.get_gpgcheck_option().get_value()) { + return; + } + + if (!is_gpg_installed()) { + return; + } + + if (!any_inbound_action(transaction)) { + return; + } + + for (auto & [hdr, expiration] : list_expired_keys()) { + std::cout << libdnf5::utils::sformat( + _("The following PGP key has expired on {}:"), + libdnf5::utils::string::format_epoch(expiration)) + << std::endl; + std::cout << libdnf5::utils::sformat(" {}", headerGetString(hdr, RPMTAG_SUMMARY)) << std::endl; + std::cout << _("For more information about the key:") << std::endl; + std::cout << libdnf5::utils::sformat(" rpm -qi {}", headerGetAsString(hdr, RPMTAG_NVR)) << std::endl; + + std::cout << _("As a result, installing packages signed with this key will fail.") << std::endl; + std::cout << _("It is recommended to remove the expired key to allow importing") << std::endl; + std::cout << _("an updated key. This might leave already installed packages unverifiable.") << std::endl + << std::endl; + + std::cout << _("The system will now proceed with removing the key.") << std::endl; + + if (libdnf5::cli::utils::userconfirm::userconfirm(config)) { + std::cout << std::endl; + if (remove_pgp_key(hdr)) { + std::cout << _("Key successfully removed.") << std::endl; + } else { + std::cout << _("Failed to remove the key.") << std::endl; + } + std::cout << std::endl; + } + + headerFree(hdr); + } +} + +} // namespace + +PluginAPIVersion libdnf_plugin_get_api_version(void) { + return PLUGIN_API_VERSION; +} + +const char * libdnf_plugin_get_name(void) { + return PLUGIN_NAME; +} + +plugin::Version libdnf_plugin_get_version(void) { + return PLUGIN_VERSION; +} + +plugin::IPlugin * libdnf_plugin_new_instance( + [[maybe_unused]] LibraryVersion library_version, + libdnf5::plugin::IPluginData & data, + libdnf5::ConfigParser & parser) try { + return new ExpiredPgpKeys(data, parser); +} catch (...) { + return nullptr; +} + +void libdnf_plugin_delete_instance(plugin::IPlugin * plugin_object) { + delete plugin_object; +} diff --git a/libdnf5-plugins/expired-pgp-keys/po/CMakeLists.txt b/libdnf5-plugins/expired-pgp-keys/po/CMakeLists.txt new file mode 100644 index 000000000..cb10bc8bd --- /dev/null +++ b/libdnf5-plugins/expired-pgp-keys/po/CMakeLists.txt @@ -0,0 +1,5 @@ +if(NOT WITH_TRANSLATIONS) + return() +endif() + +include(Translations) diff --git a/libdnf5-plugins/expired-pgp-keys/po/cs.po b/libdnf5-plugins/expired-pgp-keys/po/cs.po new file mode 100644 index 000000000..d9c5fc149 --- /dev/null +++ b/libdnf5-plugins/expired-pgp-keys/po/cs.po @@ -0,0 +1,53 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Bot , 2024. +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-07-25 06:01+0000\n" +"PO-Revision-Date: 2024-07-25 06:01+0000\n" +"Last-Translator: Bot \n" +"Language-Team: Czech \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: expired-pgp-keys.cpp:182 +#, c++-format +msgid "The following PGP key has expired on {}:" +msgstr "Následující PGP klíč vypršel dne {}:" + +#: expired-pgp-keys.cpp:186 +msgid "For more information about the key:" +msgstr "Pro více informací o klíči:" + +#: expired-pgp-keys.cpp:189 +msgid "As a result, installing packages signed with this key will fail." +msgstr "V důsledku toho se instalace balíčků podepsaných tímto klíčem nezdaří." + +#: expired-pgp-keys.cpp:190 +msgid "It is recommended to remove the expired key to allow importing" +msgstr "Doporučuje se odstranit vypršelý klíč, aby bylo možné importovat" + +#: expired-pgp-keys.cpp:191 +msgid "" +"an updated key. This might leave already installed packages unverifiable." +msgstr "" +"aktualizovaný klíč. To může zanechat již nainstalované balíčky neověřitelné." + +#: expired-pgp-keys.cpp:194 +msgid "The system will now proceed with removing the key." +msgstr "Systém nyní přistoupí k odstranění klíče." + +#: expired-pgp-keys.cpp:199 +msgid "Key successfully removed." +msgstr "Klíč byl úspěšně odstraněn." + +#: expired-pgp-keys.cpp:201 +msgid "Failed to remove the key." +msgstr "Nepodařilo se odstranit klíč." diff --git a/libdnf5-plugins/expired-pgp-keys/po/libdnf5-plugin-expired-pgp-keys.pot b/libdnf5-plugins/expired-pgp-keys/po/libdnf5-plugin-expired-pgp-keys.pot new file mode 100644 index 000000000..678d946ac --- /dev/null +++ b/libdnf5-plugins/expired-pgp-keys/po/libdnf5-plugin-expired-pgp-keys.pot @@ -0,0 +1,52 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-07-25 06:01+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: expired-pgp-keys.cpp:182 +#, c++-format +msgid "The following PGP key has expired on {}:" +msgstr "" + +#: expired-pgp-keys.cpp:186 +msgid "For more information about the key:" +msgstr "" + +#: expired-pgp-keys.cpp:189 +msgid "As a result, installing packages signed with this key will fail." +msgstr "" + +#: expired-pgp-keys.cpp:190 +msgid "It is recommended to remove the expired key to allow importing" +msgstr "" + +#: expired-pgp-keys.cpp:191 +msgid "" +"an updated key. This might leave already installed packages unverifiable." +msgstr "" + +#: expired-pgp-keys.cpp:194 +msgid "The system will now proceed with removing the key." +msgstr "" + +#: expired-pgp-keys.cpp:199 +msgid "Key successfully removed." +msgstr "" + +#: expired-pgp-keys.cpp:201 +msgid "Failed to remove the key." +msgstr ""