The idea is you define a gpgcakey for repo. This is the key that once you import will let you import everything else automatically.
If you have a cakey defined then when yum goes to import any other key it will look for a .asc detached signature for that key. if that signature is from the cakey you've already imported then yum will import the new gpgkey w/o prompting you. this works for signed packages as well as signed repomd.xml files in repos this also moves all gpg keyrings into a new per-repo persistent directory in /var/lib/yum/repos/$basearch/$releasever/repoid so we don't have to worry about a yum clean all removing our gpgkeys like we have in the past. --- output.py | 3 + yum/__init__.py | 175 +++++++++++++++++++++++++++++++++++++++++-------------- yum/config.py | 1 + yum/repos.py | 9 ++- yum/yumRepo.py | 26 +++++++- 5 files changed, 163 insertions(+), 51 deletions(-) diff --git a/output.py b/output.py index f99ab37..a36e66c 100755 --- a/output.py +++ b/output.py @@ -1228,12 +1228,15 @@ Downgrade %5.5s Package(s) def setupKeyImportCallbacks(self): confirm_func = self._cli_confirm_gpg_key_import gpg_import_func = self.getKeyForRepo + gpgca_import_func = self.getCAKeyForRepo if hasattr(self, 'prerepoconf'): self.prerepoconf.confirm_func = confirm_func self.prerepoconf.gpg_import_func = gpg_import_func + self.prerepoconf.gpgca_import_func = gpgca_import_func else: self.repos.confirm_func = confirm_func self.repos.gpg_import_func = gpg_import_func + self.repos.gpgca_import_func = gpgca_import_func def interrupt_callback(self, cbobj): '''Handle CTRL-C's during downloads diff --git a/yum/__init__.py b/yum/__init__.py index 92fa0d0..67cf5b3 100644 --- a/yum/__init__.py +++ b/yum/__init__.py @@ -85,6 +85,7 @@ from yum.rpmtrans import RPMTransaction,SimpleCliCallBack from yum.i18n import to_unicode, to_str import string +import StringIO from weakref import proxy as weakref @@ -135,6 +136,7 @@ class _YumPreRepoConf: self.interrupt_callback = None self.confirm_func = None self.gpg_import_func = None + self.gpgca_import_func = None self.cachedir = None self.cache = None @@ -415,7 +417,13 @@ class YumBase(depsolve.Depsolve): else: thisrepo.repo_config_age = repo_age thisrepo.repofile = repofn - + # repos are ver/arch specific so add $basearch/$releasever + self.conf._repos_persistdir = os.path.normpath('%s/repos/%s/%s/' + % (self.conf.persistdir, self.yumvar.get('basearch', '$basearch'), + self.yumvar.get('releasever', '$releasever'))) + thisrepo.base_persistdir = self.conf._repos_persistdir + + if thisrepo.id in self.repo_setopts: for opt in self.repo_setopts[thisrepo.id].items: if not hasattr(thisrepo, opt): @@ -575,6 +583,7 @@ class YumBase(depsolve.Depsolve): self.repos.setInterruptCallback(prerepoconf.interrupt_callback) self.repos.confirm_func = prerepoconf.confirm_func self.repos.gpg_import_func = prerepoconf.gpg_import_func + self.repos.gpgca_import_func = prerepoconf.gpgca_import_func if prerepoconf.cachedir is not None: self.repos.setCacheDir(prerepoconf.cachedir) if prerepoconf.cache is not None: @@ -4309,15 +4318,16 @@ class YumBase(depsolve.Depsolve): self.conf.obsoletes = old_conf_obs return done - def _retrievePublicKey(self, keyurl, repo=None): + def _retrievePublicKey(self, keyurl, repo=None, getSig=True): """ Retrieve a key file @param keyurl: url to the key to retrieve Returns a list of dicts with all the keyinfo """ key_installed = False - - self.logger.info(_('Retrieving GPG key from %s') % keyurl) + + msg = _('Retrieving key from %s') % keyurl + self.verbose_logger.log(logginglevels.INFO_2, msg) # Go get the GPG key from the given URL try: @@ -4336,6 +4346,33 @@ class YumBase(depsolve.Depsolve): except urlgrabber.grabber.URLGrabError, e: raise Errors.YumBaseError(_('GPG key retrieval failed: ') + to_unicode(str(e))) + + # check for a .asc file accompanying it - that's our gpg sig on the key + # suck it down and do the check + sigfile = None + valid_sig = False + if getSig and repo and repo.gpgcakey: + self.getCAKeyForRepo(repo, callback=repo.confirm_func) + try: + url = misc.to_utf8(keyurl + '.asc') + opts = repo._default_grabopts() + text = repo.id + '/gpgkeysig' + sigfile = urlgrabber.urlopen(url, **opts) + + except urlgrabber.grabber.URLGrabError, e: + sigfile = None + + if sigfile: + if not misc.valid_detached_sig(sigfile, + StringIO.StringIO(rawkey), repo.gpgcadir): + #if we decide we want to check, even though the sig failed + # here is where we would do that + raise Errors.YumBaseError(_('GPG key signature on key %s does not match CA Key for repo: %s') % (url, repo.id)) + else: + msg = _('GPG key signature verified against CA Key(s)') + self.verbose_logger.log(logginglevels.INFO_2, msg) + valid_sig = True + # Parse the key try: keys_info = misc.getgpgkeyinfo(rawkey, multiple=True) @@ -4352,29 +4389,31 @@ class YumBase(depsolve.Depsolve): _('GPG key parsing failed: key does not have value %s') + info thiskey[info] = keyinfo[info] thiskey['hexkeyid'] = misc.keyIdToRPMVer(keyinfo['keyid']).upper() + thiskey['valid_sig'] = valid_sig + thiskey['has_sig'] = bool(sigfile) keys.append(thiskey) return keys - def _getKeyImportMessage(self, info, keyurl): + def _getKeyImportMessage(self, info, keyurl, keytype='GPG'): msg = None if keyurl.startswith("file:"): fname = keyurl[len("file:"):] pkgs = self.rpmdb.searchFiles(fname) if pkgs: pkgs = sorted(pkgs)[-1] - msg = (_('Importing GPG key 0x%s:\n' + msg = (_('Importing %s key 0x%s:\n' ' Userid : %s\n' ' Package: %s (%s)\n' ' From : %s') % - (info['hexkeyid'], to_unicode(info['userid']), + (keytype, info['hexkeyid'], to_unicode(info['userid']), pkgs, pkgs.ui_from_repo, keyurl.replace("file://",""))) if msg is None: - msg = (_('Importing GPG key 0x%s:\n' + msg = (_('Importing %s key 0x%s:\n' ' Userid: "%s"\n' ' From : %s') % - (info['hexkeyid'], to_unicode(info['userid']), + (keytype, info['hexkeyid'], to_unicode(info['userid']), keyurl.replace("file://",""))) self.logger.critical("%s", msg) @@ -4405,24 +4444,34 @@ class YumBase(depsolve.Depsolve): self.logger.info(_('GPG key at %s (0x%s) is already installed') % ( keyurl, info['hexkeyid'])) continue - - # Try installing/updating GPG key - self._getKeyImportMessage(info, keyurl) - rc = False - if self.conf.assumeyes: - rc = True - elif fullaskcb: - rc = fullaskcb({"po": po, "userid": info['userid'], - "hexkeyid": info['hexkeyid'], - "keyurl": keyurl, - "fingerprint": info['fingerprint'], - "timestamp": info['timestamp']}) - elif askcb: - rc = askcb(po, info['userid'], info['hexkeyid']) - - if not rc: - raise Errors.YumBaseError, _("Not installing key") + if repo.gpgcakey and info['has_sig'] and info['valid_sig']: + key_installed = True + else: + # Try installing/updating GPG key + self._getKeyImportMessage(info, keyurl) + rc = False + if self.conf.assumeyes: + rc = True + + # grab the .sig/.asc for the keyurl, if it exists + # if it does check the signature on the key + # if it is signed by one of our ca-keys for this repo or the global one + # then rc = True + # else ask as normal. + + elif fullaskcb: + rc = fullaskcb({"po": po, "userid": info['userid'], + "hexkeyid": info['hexkeyid'], + "keyurl": keyurl, + "fingerprint": info['fingerprint'], + "timestamp": info['timestamp']}) + elif askcb: + rc = askcb(po, info['userid'], info['hexkeyid']) + + if not rc: + raise Errors.YumBaseError, _("Not installing key") + # Import the key ts = self.rpmdb.readOnlyTS() result = ts.pgpImportPubkey(misc.procgpgkey(info['raw_key'])) @@ -4446,43 +4495,55 @@ class YumBase(depsolve.Depsolve): self.logger.info(_("Import of key(s) didn't help, wrong key(s)?")) raise Errors.YumBaseError, errmsg - def getKeyForRepo(self, repo, callback=None): + def _getAnyKeyForRepo(self, repo, destdir, keyurl_list, is_cakey=False, callback=None): """ Retrieve a key for a repository If needed, prompt for if the key should be imported using callback @param repo: Repository object to retrieve the key of. + @param destdir: destination of the gpg pub ring + @param keyurl_list: list of urls for gpg keys + @param is_cakey: bool - are we pulling in a ca key or not @param callback: Callback function to use for asking for verification of a key. Takes a dictionary of key info. """ - keyurls = repo.gpgkey + key_installed = False - for keyurl in keyurls: - keys = self._retrievePublicKey(keyurl, repo) + for keyurl in keyurl_list: + keys = self._retrievePublicKey(keyurl, repo, getSig=not is_cakey) for info in keys: # Check if key is already installed - if info['keyid'] in misc.return_keyids_from_pubring(repo.gpgdir): + if hex(int(info['keyid']))[2:-1].upper() in misc.return_keyids_from_pubring(destdir): self.logger.info(_('GPG key at %s (0x%s) is already imported') % ( keyurl, info['hexkeyid'])) + key_installed = True continue # Try installing/updating GPG key - self._getKeyImportMessage(info, keyurl) - rc = False - if self.conf.assumeyes: - rc = True - elif callback: - rc = callback({"repo": repo, "userid": info['userid'], - "hexkeyid": info['hexkeyid'], "keyurl": keyurl, - "fingerprint": info['fingerprint'], - "timestamp": info['timestamp']}) - - - if not rc: - raise Errors.YumBaseError, _("Not installing key for repo %s") % repo + if is_cakey: + keytype = 'CA' + else: + keytype = 'GPG' + + if repo.gpgcakey and info['has_sig'] and info['valid_sig']: + key_installed = True + else: + self._getKeyImportMessage(info, keyurl, keytype) + rc = False + if self.conf.assumeyes: + rc = True + elif callback: + rc = callback({"repo": repo, "userid": info['userid'], + "hexkeyid": info['hexkeyid'], "keyurl": keyurl, + "fingerprint": info['fingerprint'], + "timestamp": info['timestamp']}) + + + if not rc: + raise Errors.YumBaseError, _("Not installing key for repo %s") % repo # Import the key - result = misc.import_key_to_pubring(info['raw_key'], info['hexkeyid'], gpgdir=repo.gpgdir) + result = misc.import_key_to_pubring(info['raw_key'], info['hexkeyid'], gpgdir=destdir) if not result: raise Errors.YumBaseError, _('Key import failed') self.logger.info(_('Key imported successfully')) @@ -4495,6 +4556,29 @@ class YumBase(depsolve.Depsolve): 'Check that the correct key URLs are configured for ' \ 'this repository.') % (repo.name) + def getKeyForRepo(self, repo, callback=None): + """ + Retrieve a key for a repository If needed, prompt for if the key should + be imported using callback + + @param repo: Repository object to retrieve the key of. + @param callback: Callback function to use for asking for verification + of a key. Takes a dictionary of key info. + """ + self._getAnyKeyForRepo(repo, repo.gpgdir, repo.gpgkey, is_cakey=False, callback=callback) + + def getCAKeyForRepo(self, repo, callback=None): + """ + Retrieve a key for a repository If needed, prompt for if the key should + be imported using callback + + @param repo: Repository object to retrieve the key of. + @param callback: Callback function to use for asking for verification + of a key. Takes a dictionary of key info. + """ + + self._getAnyKeyForRepo(repo, repo.gpgcadir, repo.gpgcakey, is_cakey=True, callback=callback) + def _limit_installonly_pkgs(self): """ Limit packages based on conf.installonly_limit, if any of the packages being installed have a provide in conf.installonlypkgs. @@ -4772,6 +4856,7 @@ class YumBase(depsolve.Depsolve): newrepo.gpgcheck = self.conf.gpgcheck newrepo.repo_gpgcheck = self.conf.repo_gpgcheck newrepo.basecachedir = self.conf.cachedir + newrepo.base_persistdir = self.conf._repos_persistdir for key in kwargs.keys(): if not hasattr(newrepo, key): continue # skip the ones which aren't vars diff --git a/yum/config.py b/yum/config.py index 14eb992..97e5e3d 100644 --- a/yum/config.py +++ b/yum/config.py @@ -794,6 +794,7 @@ class RepoConf(BaseConfig): metalink = UrlOption() mediaid = Option() gpgkey = UrlListOption() + gpgcakey = UrlListOption() exclude = ListOption() includepkgs = ListOption() diff --git a/yum/repos.py b/yum/repos.py index 4b74ac6..f11ed52 100644 --- a/yum/repos.py +++ b/yum/repos.py @@ -32,9 +32,12 @@ class _wrap_ayum_getKeyForRepo: we have a seperate class). A "better" fix might be to explicitly pass the YumBase instance to the callback ... API change! """ - def __init__(self, ayum): + def __init__(self, ayum, ca=False): self.ayum = weakref(ayum) + self.ca = ca def __call__(self, repo, callback=None): + if self.ca: + return self.ayum.getCAKeyForRepo(repo, callback) return self.ayum.getKeyForRepo(repo, callback) class RepoStorage: @@ -57,6 +60,7 @@ class RepoStorage: # even quasi-useful # defaults to what is probably sane-ish self.gpg_import_func = _wrap_ayum_getKeyForRepo(ayum) + self.gpgca_import_func = _wrap_ayum_getKeyForRepo(ayum, ca=True) self.confirm_func = None # This allow listEnabled() to be O(1) most of the time. @@ -77,7 +81,8 @@ class RepoStorage: for repo in repos: repo.setup(self.ayum.conf.cache, self.ayum.mediagrabber, - gpg_import_func = self.gpg_import_func, confirm_func=self.confirm_func) + gpg_import_func = self.gpg_import_func, confirm_func=self.confirm_func, + gpgca_import_func = self.gpgca_import_func) # if we come back from setup NOT enabled then mark as disabled # so nothing else touches us if not repo.enabled: diff --git a/yum/yumRepo.py b/yum/yumRepo.py index b0e23c6..4926e9d 100644 --- a/yum/yumRepo.py +++ b/yum/yumRepo.py @@ -255,6 +255,7 @@ class YumRepository(Repository, config.RepoConf): # config is very, very old # throw in some stubs for things that will be set by the config class self.basecachedir = "" + self.base_persistdir = "" self.cost = 1000 self.copy_local = 0 # holder for stuff we've grabbed @@ -273,6 +274,7 @@ class YumRepository(Repository, config.RepoConf): # callbacks for gpg key importing and confirmation self.gpg_import_func = None + self.gpgca_import_func = None self.confirm_func = None # The reason we want to turn this off are things like repoids @@ -368,7 +370,8 @@ class YumRepository(Repository, config.RepoConf): # we exclude all vars which start with _ or are in this list: excluded_vars = ('mediafunc', 'sack', 'metalink_data', 'grab', 'grabfunc', 'repoXML', 'cfg', 'retrieved', - 'mirrorlistparsed', 'gpg_import_func', 'failure_obj', + 'mirrorlistparsed', 'gpg_import_func', + 'gpgca_import_func', 'failure_obj', 'callback', 'confirm_func', 'groups_added', 'interrupt_callback', 'id', 'mirror_failure_obj', 'repo_config_age', 'groupsfilename', 'copy_local', @@ -541,12 +544,15 @@ class YumRepository(Repository, config.RepoConf): """make the necessary dirs, if possible, raise on failure""" cachedir = os.path.join(self.basecachedir, self.id) + persistdir = os.path.join(self.base_persistdir, self.id) pkgdir = os.path.join(cachedir, 'packages') hdrdir = os.path.join(cachedir, 'headers') self.setAttribute('_dir_setup_cachedir', cachedir) self.setAttribute('_dir_setup_pkgdir', pkgdir) self.setAttribute('_dir_setup_hdrdir', hdrdir) - self.setAttribute('_dir_setup_gpgdir', self.cachedir + '/gpgdir') + self.setAttribute('_dir_setup_persistdir', persistdir) + self.setAttribute('_dir_setup_gpgdir', persistdir + '/gpgdir') + self.setAttribute('_dir_setup_gpgcadir', persistdir + '/gpgcadir') cookie = self.cachedir + '/' + self.metadata_cookie_fn self.setAttribute('_dir_setup_metadata_cookie', cookie) @@ -554,6 +560,14 @@ class YumRepository(Repository, config.RepoConf): for dir in [self.cachedir, self.pkgdir]: self._dirSetupMkdir_p(dir) + # persistdir is really root-only but try the make anyway and just + # catch the exception + for dir in [self.persistdir]: + try: + self._dirSetupMkdir_p(dir) + except Errors.RepoError, e: + pass + # if we're using a cachedir that's not the system one, copy over these # basic items from the system one self._preload_md_from_system_cache('repomd.xml') @@ -583,12 +597,16 @@ class YumRepository(Repository, config.RepoConf): self._dirSetupMkdir_p(val) return ret cachedir = property(lambda self: self._dirGetAttr('cachedir')) + persistdir = property(lambda self: self._dirGetAttr('persistdir')) + pkgdir = property(lambda self: self._dirGetAttr('pkgdir'), lambda self, x: self._dirSetAttr('pkgdir', x)) hdrdir = property(lambda self: self._dirGetAttr('hdrdir'), lambda self, x: self._dirSetAttr('hdrdir', x)) gpgdir = property(lambda self: self._dirGetAttr('gpgdir'), lambda self, x: self._dirSetAttr('gpgdir', x)) + gpgcadir = property(lambda self: self._dirGetAttr('gpgcadir'), + lambda self, x: self._dirSetAttr('gpgcadir', x)) metadata_cookie = property(lambda self: self._dirGetAttr('metadata_cookie')) def baseurlSetup(self): @@ -947,11 +965,12 @@ class YumRepository(Repository, config.RepoConf): fo.close() del fo - def setup(self, cache, mediafunc = None, gpg_import_func=None, confirm_func=None): + def setup(self, cache, mediafunc = None, gpg_import_func=None, confirm_func=None, gpgca_import_func=None): try: self.cache = cache self.mediafunc = mediafunc self.gpg_import_func = gpg_import_func + self.gpgca_import_func = gpgca_import_func self.confirm_func = confirm_func except Errors.RepoError, e: raise @@ -1451,7 +1470,6 @@ class YumRepository(Repository, config.RepoConf): size=102400) except URLGrabError, e: raise URLGrabError(-1, 'Error finding signature for repomd.xml for %s: %s' % (self, e)) - valid = misc.valid_detached_sig(result, filepath, self.gpgdir) if not valid and self.gpg_import_func: try: -- 1.7.2.3 _______________________________________________ Yum-devel mailing list Yum-devel@lists.baseurl.org http://lists.baseurl.org/mailman/listinfo/yum-devel