1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 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 resolved(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
|