diff --git a/dnf-plugins-core.spec b/dnf-plugins-core.spec index f0984dd8..91c49a12 100644 --- a/dnf-plugins-core.spec +++ b/dnf-plugins-core.spec @@ -566,6 +566,7 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/ %{_mandir}/man8/dnf*-debug.* %{_mandir}/man8/dnf*-debuginfo-install.* %{_mandir}/man8/dnf*-download.* +%{_mandir}/man8/dnf*-expired-pgp-keys.* %{_mandir}/man8/dnf*-generate_completion_cache.* %{_mandir}/man8/dnf*-groups-manager.* %{_mandir}/man8/dnf*-needs-restarting.* @@ -619,6 +620,7 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/ %config(noreplace) %{_sysconfdir}/dnf/plugins/copr.conf %config(noreplace) %{_sysconfdir}/dnf/plugins/copr.d %config(noreplace) %{_sysconfdir}/dnf/plugins/debuginfo-install.conf +%config(noreplace) %{_sysconfdir}/dnf/plugins/expired-pgp-keys.conf %{python3_sitelib}/dnf-plugins/builddep.py %{python3_sitelib}/dnf-plugins/changelog.py %{python3_sitelib}/dnf-plugins/config_manager.py @@ -626,6 +628,7 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/ %{python3_sitelib}/dnf-plugins/debug.py %{python3_sitelib}/dnf-plugins/debuginfo-install.py %{python3_sitelib}/dnf-plugins/download.py +%{python3_sitelib}/dnf-plugins/expired-pgp-keys.py %{python3_sitelib}/dnf-plugins/generate_completion_cache.py %{python3_sitelib}/dnf-plugins/groups_manager.py %{python3_sitelib}/dnf-plugins/needs_restarting.py @@ -642,6 +645,7 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/ %{python3_sitelib}/dnf-plugins/__pycache__/debug.* %{python3_sitelib}/dnf-plugins/__pycache__/debuginfo-install.* %{python3_sitelib}/dnf-plugins/__pycache__/download.* +%{python3_sitelib}/dnf-plugins/__pycache__/expired-pgp-keys.* %{python3_sitelib}/dnf-plugins/__pycache__/generate_completion_cache.* %{python3_sitelib}/dnf-plugins/__pycache__/groups_manager.* %{python3_sitelib}/dnf-plugins/__pycache__/needs_restarting.* diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 9fd3f7c4..4e7a0f88 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -25,6 +25,7 @@ INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/dnf4-builddep.8 ${CMAKE_CURRENT_BINARY_DIR}/dnf4-debug.8 ${CMAKE_CURRENT_BINARY_DIR}/dnf4-debuginfo-install.8 ${CMAKE_CURRENT_BINARY_DIR}/dnf4-download.8 + ${CMAKE_CURRENT_BINARY_DIR}/dnf4-expired-pgp-keys.8 ${CMAKE_CURRENT_BINARY_DIR}/dnf4-generate_completion_cache.8 ${CMAKE_CURRENT_BINARY_DIR}/dnf4-groups-manager.8 ${CMAKE_CURRENT_BINARY_DIR}/dnf4-leaves.8 diff --git a/doc/conf.py b/doc/conf.py index ea14cbd7..57d3515a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -249,6 +249,7 @@ def version_readout(): ('debug', 'dnf4-debug', u'DNF debug Plugin', AUTHORS, 8), ('debuginfo-install', 'dnf4-debuginfo-install', u'DNF debuginfo-install Plugin', AUTHORS, 8), ('download', 'dnf4-download', u'DNF download Plugin', AUTHORS, 8), + ('expired-pgp-keys', 'dnf4-expired-pgp-keys', u'DNF expired-pgp-keys Plugin', AUTHORS, 8), ('generate_completion_cache', 'dnf4-generate_completion_cache', u'DNF generate_completion_cache Plugin', AUTHORS, 8), ('groups-manager', 'dnf4-groups-manager', u'DNF groups-manager Plugin', AUTHORS, 8), diff --git a/doc/expired-pgp-keys.rst b/doc/expired-pgp-keys.rst new file mode 100644 index 00000000..e1eaa368 --- /dev/null +++ b/doc/expired-pgp-keys.rst @@ -0,0 +1,22 @@ +=========================== +DNF 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/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/index.rst b/doc/index.rst index 5aed04bd..501acc0e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -32,6 +32,7 @@ This documents core plugins of DNF: debug debuginfo-install download + expired-pgp-keys generate_completion_cache groups-manager leaves diff --git a/etc/dnf/plugins/CMakeLists.txt b/etc/dnf/plugins/CMakeLists.txt index 799ade58..41c46609 100644 --- a/etc/dnf/plugins/CMakeLists.txt +++ b/etc/dnf/plugins/CMakeLists.txt @@ -3,6 +3,7 @@ ADD_SUBDIRECTORY (post-transaction-actions.d) ADD_SUBDIRECTORY (pre-transaction-actions.d) INSTALL (FILES copr.conf DESTINATION ${SYSCONFDIR}/dnf/plugins) INSTALL (FILES debuginfo-install.conf DESTINATION ${SYSCONFDIR}/dnf/plugins) +INSTALL (FILES expired-pgp-keys.conf DESTINATION ${SYSCONFDIR}/dnf/plugins) if (${WITHOUT_LOCAL} STREQUAL "0") INSTALL (FILES local.conf DESTINATION ${SYSCONFDIR}/dnf/plugins) endif() diff --git a/etc/dnf/plugins/expired-pgp-keys.conf b/etc/dnf/plugins/expired-pgp-keys.conf new file mode 100644 index 00000000..4ed5e8c3 --- /dev/null +++ b/etc/dnf/plugins/expired-pgp-keys.conf @@ -0,0 +1,2 @@ +[main] +enabled = 0 \ No newline at end of file diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 6f4fa6e6..38e10b0f 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -5,6 +5,7 @@ INSTALL (FILES debuginfo-install.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugin INSTALL (FILES config_manager.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) INSTALL (FILES copr.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) INSTALL (FILES download.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) +INSTALL (FILES expired-pgp-keys.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) INSTALL (FILES generate_completion_cache.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) INSTALL (FILES groups_manager.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) INSTALL (FILES leaves.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins) diff --git a/plugins/expired-pgp-keys.py b/plugins/expired-pgp-keys.py new file mode 100644 index 00000000..9a12b8b4 --- /dev/null +++ b/plugins/expired-pgp-keys.py @@ -0,0 +1,121 @@ +import dnf +import rpm +import subprocess + +from datetime import datetime +from dnfpluginscore import _, logger + + +class ExpiredPGPKeys(dnf.Plugin): + """ + Find expired PGP keys and suggest their removal. + + This is a workaround to solve https://github.com/rpm-software-management/dnf/issues/2075. + """ + + name = 'expired-pgp-keys' + + def __init__(self, base, cli): + super(ExpiredPGPKeys, self).__init__(base, cli) + self.base = base + self.cli = cli + + def pre_transaction(self): + if not self.base.conf.gpgcheck: + return + + if not self.is_gpg_installed(): + return + + if not self._any_forward_action(): + return + + for (hdr, expire_date) in self.list_expired_keys(): + print(_("The following PGP key has expired on {0}:".format(expire_date))) + print(" {0}\n".format(hdr["summary"])) + print(_("For more information about the key:")) + print(" rpm -qi {0}\n".format(hdr[rpm.RPMTAG_NVR])) + + print(_("As a result, installing packages signed with this key will fail.\n" + "It is recommended to remove the expired key to allow importing\n" + "an updated key. This might leave already installed packages unverifiable.")) + + if self._ask_user_no_raise(_("Do you want to remove the key?")): + print() + if self.remove_pgp_key(hdr): + print(_("Key successfully removed.")) + else: + print(_("Failed to remove the key.")) + print() + + @staticmethod + def is_gpg_installed(): + """ + Check that GPG is installed to enable querying expired keys later. + """ + ts = rpm.TransactionSet() + mi = ts.dbMatch("provides", "gpg") + return len(mi) > 0 + + @staticmethod + def remove_pgp_key(hdr): + """ + Remove the system package corresponding to the PGP key from the given RPM header. + """ + ts = rpm.TransactionSet() + ts.addErase(hdr) + error = ts.run(lambda *_: None, '') + return not error + + @staticmethod + def list_expired_keys(): + """ + 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. + """ + ts = rpm.TransactionSet() + mi = ts.dbMatch("name", "gpg-pubkey") + expired_keys = [] + for hdr in mi: + date = ExpiredPGPKeys.get_key_expire_date(hdr) + if date and date < datetime.now(): + expired_keys.append((hdr, date)) + return expired_keys + + @staticmethod + def get_key_expire_date(hdr): + """ + Retrieve the PGP key expiration date, or return None if the expiration is not available. + """ + + try: + # show formatted output of the pgp key + gpg_key_ps = subprocess.run(("gpg", "--show-keys", "--with-colon"), + input=hdr[rpm.RPMTAG_DESCRIPTION], + capture_output=True, text=True, check=True) + + # parse the pgp key expiration time + # see also https://github.com/gpg/gnupg/blob/master/doc/DETAILS#field-7---expiration-date + expire_date_string = gpg_key_ps.stdout.split('\n')[0].split(':')[6] + if not expire_date_string.isnumeric(): + return None + + return datetime.fromtimestamp(float(expire_date_string)) + except subprocess.CalledProcessError as e: + logger.debug('Error when checking expired pgp keys: %s', str(e)) + return None + + def _any_forward_action(self): + for tsi in self.base.transaction: + if tsi.action in dnf.transaction.FORWARD_ACTIONS: + return True + return False + + def _ask_user_no_raise(self, msg): + if self.base._promptWanted(): + if self.base.conf.assumeno or not self.base.output.userconfirm( + msg='{} [y/N]: '.format(msg), + defaultyes_msg='\n{} [Y/n]: '.format(msg)): + return False + return True