From d25f05feb4cd07c1371443ff4941f96942e51e33 Mon Sep 17 00:00:00 2001 From: Fabio Erculiani Date: Mon, 4 Jan 2010 00:25:10 +0100 Subject: [PATCH] [entropy.security] add GPG support --- libraries/entropy/security.py | 258 ++++++++++++++++++++++++++++++++-- 1 file changed, 247 insertions(+), 11 deletions(-) diff --git a/libraries/entropy/security.py b/libraries/entropy/security.py index 5291d5610..6050bc57d 100644 --- a/libraries/entropy/security.py +++ b/libraries/entropy/security.py @@ -16,13 +16,16 @@ import os import shutil import subprocess import datetime +import tempfile + from entropy.exceptions import EntropyException from entropy.misc import LogFile from entropy.const import etpConst, etpUi, const_setup_perms, \ const_debug_write from entropy.i18n import _ -from entropy.output import blue, bold, red, darkgreen, darkred +from entropy.output import blue, bold, red, darkgreen, darkred, purple, brown from entropy.cache import EntropyCacher + import entropy.tools class System: @@ -98,6 +101,13 @@ class System: self.advisories_changed = None self.adv_metadata = None self.affected_atoms = set() + self.__gpg_keystore_dir = os.path.join(etpConst['confdir'], + "security-advisories-keys") + + self._gpg_feature = True + env_gpg = os.getenv('ETP_DISBLE_GPG') + if env_gpg is not None: + self._gpg_feature = False from xml.dom import minidom self.minidom = minidom @@ -125,9 +135,16 @@ class System: self.security_url = security_url self.unpacked_package = os.path.join(self.unpackdir, "glsa_package") self.security_url_checksum = security_url + md5_ext + self.security_url_gpg_pubkey = os.path.join( + os.path.dirname(security_url), etpConst['etpdatabasegpgfile']) + self.security_url_gpg_sign = security_url + etpConst['etpgpgextension'] self.download_package = os.path.join(self.unpackdir, security_file) self.download_package_checksum = self.download_package + md5_ext + self.download_package_gpg_pubkey = os.path.join(self.unpackdir, + "security-advisories#" + etpConst['etpdatabasegpgfile']) + self.download_package_gpg_sign = self.download_package + \ + etpConst['etpgpgextension'] self.old_download_package_checksum = os.path.join( System._CACHE_DIR, os.path.basename(security_url)) + md5_ext @@ -188,6 +205,26 @@ class System: return self.__generic_download(self.security_url_checksum, self.download_package_checksum, show_speed = False) + def __download_glsa_package_gpg_sign(self): + """ + Download GLSA compressed package checksum (md5) from a trusted source. + """ + # remove old + if os.path.isfile(self.download_package_gpg_sign): + os.remove(self.download_package_gpg_sign) + return self.__generic_download(self.security_url_gpg_sign, + self.download_package_gpg_sign, show_speed = False) + + def __download_glsa_package_gpg_pubkey(self): + """ + Download GLSA compressed package checksum (md5) from a trusted source. + """ + # remove old + if os.path.isfile(self.download_package_gpg_pubkey): + os.remove(self.download_package_gpg_pubkey) + return self.__generic_download(self.security_url_gpg_pubkey, + self.download_package_gpg_pubkey, show_speed = False) + def __generic_download(self, url, save_to, show_speed = True): """ Generic, secure, URL download method. @@ -212,6 +249,192 @@ class System: self.Entropy.setup_default_file_perms(save_to) return True + def __load_gpg(self): + try: + repo_sec = self.Entropy.RepositorySecurity( + keystore_dir = self.__gpg_keystore_dir) + except RepositorySecurity.GPGError: + return None # GPG not available + return repo_sec + + def __install_gpg_key(self, repo_sec): + pk_expired = False + try: + pk_avail = repo_sec.is_pubkey_available(self.security_url) + except repo_sec.KeyExpired: + pk_avail = False + pk_expired = True + + def do_warn_user(fingerprint): + mytxt = purple(_("Make sure to verify the imported key and set an appropriate trust level")) + self.Entropy.updateProgress( + mytxt + ":", + type = "warning", + header = red(" # ") + ) + mytxt = brown("gpg --homedir '%s' --edit-key '%s'" % ( + self.__gpg_keystore_dir, fingerprint,) + ) + self.Entropy.updateProgress( + "$ " + mytxt, + type = "warning", + header = red(" # ") + ) + + easy_url = entropy.tools.spliturl(self.security_url).netloc + + if pk_avail: + + tmp_dir = tempfile.mkdtemp() + repo_tmp_sec = self.Entropy.RepositorySecurity( + keystore_dir = tmp_dir) + # try to install and get fingerprint + try: + downloaded_key_fp = repo_tmp_sec.install_key( + self.security_url, self.download_package_gpg_pubkey) + except repo_sec.GPGError: + downloaded_key_fp = None + + fingerprint = \ + repo_sec.get_key_metadata(self.security_url)['fingerprint'] + shutil.rmtree(tmp_dir, True) + + if downloaded_key_fp != fingerprint and \ + (downloaded_key_fp is not None): + mytxt = "%s: %s !!!" % ( + purple(_("GPG key changed for")), + bold(easy_url), + ) + self.Entropy.updateProgress( + mytxt, + type = "warning", + header = red(" # ") + ) + mytxt = "[%s => %s]" % ( + darkgreen(fingerprint), + purple(downloaded_key_fp), + ) + self.Entropy.updateProgress( + mytxt, + type = "warning", + header = red(" # ") + ) + else: + mytxt = "%s: %s" % ( + purple(_("GPG key already installed for")), + bold(easy_url), + ) + self.Entropy.updateProgress( + mytxt, + type = "info", + header = red(" # ") + ) + do_warn_user(fingerprint) + return True + + elif pk_expired: + mytxt = "%s: %s" % ( + purple(_("GPG key EXPIRED for URL")), + bold(easy_url), + ) + self.Entropy.updateProgress( + mytxt, + type = "warning", + header = red(" # ") + ) + + # actually install + mytxt = "%s: %s" % ( + purple(_("Installing GPG key for URL")), + brown(easy_url), + ) + self.Entropy.updateProgress( + mytxt, + type = "info", + header = red(" # "), + back = True + ) + try: + fingerprint = repo_sec.install_key(self.security_url, + self.download_package_gpg_pubkey) + except repo_sec.GPGError as err: + mytxt = "%s: %s" % ( + darkred(_("Error during GPG key installation")), + err, + ) + self.Entropy.updateProgress( + mytxt, + type = "error", + header = red(" # ") + ) + return False + + mytxt = "%s: %s" % ( + purple(_("Successfully installed GPG key for URL")), + brown(easy_url), + ) + self.Entropy.updateProgress( + mytxt, + type = "info", + header = red(" # ") + ) + mytxt = "%s: %s" % ( + darkgreen(_("Fingerprint")), + bold(fingerprint), + ) + self.Entropy.updateProgress( + mytxt, + type = "info", + header = red(" # ") + ) + do_warn_user(fingerprint) + return True + + def __verify_gpg(self): + + repo_sec = self.__load_gpg() + if repo_sec is None: + return None + installed = self.__install_gpg_key(repo_sec) + if not installed: + return None + + # verify GPG now + gpg_good, err_msg = repo_sec.verify_file(self.security_url, + self.download_package, self.download_package_gpg_sign) + if not gpg_good: + mytxt = "%s: %s" % ( + purple(_("Error during GPG verification of")), + os.path.basename(self.download_package), + ) + self.Entropy.updateProgress( + mytxt, + type = "error", + header = red(" # ") + bold(" !!! ") + ) + mytxt = "%s: %s" % ( + purple(_("It could mean a potential security risk")), + err_msg, + ) + self.Entropy.updateProgress( + mytxt, + type = "error", + header = red(" # ") + bold(" !!! ") + ) + return False + + mytxt = "%s: %s." % ( + bold(_("Security Advisories")), + purple(_("GPG key verification successful")), + ) + self.Entropy.updateProgress( + mytxt, + type = "info", + header = red(" # ") + ) + + return True + def __get_downloaded_package_checksum(self): if not os.path.isfile(self.download_package_checksum) or \ @@ -744,13 +967,7 @@ class System: @return: availability @rtype: bool """ - if not os.path.lexists(etpConst['securitydir']): - return False - if not os.path.isdir(etpConst['securitydir']): - return False - else: - return True - return False + return os.path.isdir(etpConst['securitydir']) def sync(self, do_cache = True, force = False): """ @@ -930,7 +1147,7 @@ class System: elif status == 0: mytxt = "%s: %s." % ( bold(_("Security Advisories")), - darkgreen(_("verification Successful")), + darkgreen(_("verification successful")), ) self.Entropy.updateProgress( mytxt, @@ -939,8 +1156,27 @@ class System: header = red(" # ") ) else: - mytxt = _("Return status not valid") - raise InvalidData("InvalidData: %s." % (mytxt,)) + raise System.UpdateError("Unhandled return code: %s" % (status,)) + + # download GPG key and package signature in a row + # env hook, disable GPG check + if self._gpg_feature: + gpg_sign_sts = self.__download_glsa_package_gpg_sign() + gpg_key_sts = self.__download_glsa_package_gpg_pubkey() + if gpg_sign_sts and gpg_key_sts: + verify_sts = self.__verify_gpg() + if verify_sts is None: + mytxt = "%s: %s." % ( + bold(_("Security Advisories")), + purple(_("GPG service not available")), + ) + self.Entropy.updateProgress( + mytxt, + type = "info", + header = red(" # ") + ) + elif not verify_sts: + return 7 # save downloaded md5 if os.path.isfile(self.download_package_checksum) and \