Skip to content

Commit

Permalink
expired-pgp-keys: New plugin for detecting expired PGP keys
Browse files Browse the repository at this point in the history
Workaround for: rpm-software-management/dnf#2075

Co-authored-by: Jakub Kadlcik <[email protected]>
  • Loading branch information
jan-kolarik and FrostyX committed Jul 4, 2024
1 parent a054dcb commit 29b098e
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 0 deletions.
4 changes: 4 additions & 0 deletions dnf-plugins-core.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -619,13 +620,15 @@ 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
%{python3_sitelib}/dnf-plugins/copr.py
%{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
Expand All @@ -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.*
Expand Down
1 change: 1 addition & 0 deletions doc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
22 changes: 22 additions & 0 deletions doc/expired-pgp-keys.rst
Original file line number Diff line number Diff line change
@@ -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 ``True``.
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This documents core plugins of DNF:
debug
debuginfo-install
download
expired-pgp-keys
generate_completion_cache
groups-manager
leaves
Expand Down
1 change: 1 addition & 0 deletions etc/dnf/plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions etc/dnf/plugins/expired-pgp-keys.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[main]
enabled = 1
1 change: 1 addition & 0 deletions plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
121 changes: 121 additions & 0 deletions plugins/expired-pgp-keys.py
Original file line number Diff line number Diff line change
@@ -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."))

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

0 comments on commit 29b098e

Please sign in to comment.