auto importing rpm gpg public keys from keyserver

Jeff Johnson n3npq at mac.com
Thu Jun 15 12:33:51 PDT 2006


On Jun 15, 2006, at 3:15 PM, Andrea Arcangeli wrote:

> This should look a bit better, this works fine with both console  
> and gui
> too (tested). It's still non-intrusive, if you don't set
> keyserver=something, it does absolutely nothing (it only tells you,  
> that
> you can enable the feature if you want).
>
> I don't understand what the _ is about (I guess it has to do with
> translations), so I omitted it (if you can check a fingerprint chances
> are you don't need a translation).
>

Hmmm, which underscore? Likelier a typo bug than translations ...

> I'm quite satisified by how it works, even if it's still the simple
> approach of going with max-performance and not doing the whole  
> checking
> outside rpm beforehand.
>

I'm much happier w/o tghe popen ...

> Ideally I'd like to run a first hdrFromFdno as user nobody to make any
> rpm bug become irrelevant in terms of gaining root with a malicious
> rpm-header payload. However that is orthogonal and it
> would slowdown performance too. How likely it is to have bugs in  
> the rpm
> header parsing code? Did it ever happen? Anyway it's certainly _a_lot_
> more secure this, than to keep signatures off by default. Exploiting a
> buffer overflow is a problem in itself, so there are already multiple
> layers of bugs to exploit before you can gain root with this patch
> applied, certainly it would be nicer to add one more layer by parsing
> the header the first time as user nobody but this seems a reasonable
> start.
>

The parser in rpm-4.1 was tested by exhaustively mangling a header
with all possible changes, fixing every segfault. The parser has  
changed little
since then, but ...

The risk of exploit is one of design. rpm was *supposed* to segfault on
damaged data. That, was a much cheerier world than now.

There has been only one serious BugTraq exploit for rpm, a double  
free long since
fixed, not with the OpenPGP parser.

However, the header loading is far more complicated than a simple  
read, and the
signatures are inconveniently located in a header. Basically,  
ts.hdrFromFdno()
has an entirely different code path to extract the signature from the  
overly complicated
header container before the pkg metadata is read. Once the signature  
has been extracted
from the package, the remaining audit is straightforward, 3 reads to  
get plaintext blob,
compute digest, verify signature.

> The end of the patch turns signature checking on by default, and  
> returns
> false by default in the interface of the Interface (it's always a  
> safer
> default to answer no if unsure, either no or raise an exception). In
> practice it should be a noop.
>
> If the package isn't signed it should fail in gpg but that's ok.
>

Make sure that you read the original flags, and then mask on your per- 
application
flags, please. That way per-system defaults can be set from  
configuration even if applications
wish to override specific behavior. The old value is returned by  
ts.setVSFlags() iirc,
and there is a ts.VSFlags() getter as well. All from memory, holler  
and I'll actually look
at the code, if not. ;-)

Otherwise, I shall be forced to make ts.setVSFlags a noop one of  
these days. I'd rather not do that.

And thank you for making signature checking opt-out rather than opt- 
in in smart.

73 de Jeff

> Index: smart/backends/rpm/pm.py
> --- smart/backends/rpm/pm.py.~1~	2006-06-15 03:03:07.000000000 +0200
> +++ smart/backends/rpm/pm.py	2006-06-15 21:04:05.000000000 +0200
> @@ -34,12 +34,24 @@ import thread
>  import errno
>  import fcntl
>  import time
> +import popen2
>
>  try:
>      ENCODING = locale.getpreferredencoding()
>  except locale.Error:
>      ENCODING = "C"
>
> +def get_rpm_public_key(filepath):
> +    f = file(filepath, 'r')
> +    ts = rpm.ts()
> +    ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
> +    hdr = ts.hdrFromFdno(f.fileno())
> +    return hdr.sprintf("%|DSAHEADER?{%{DSAHEADER:pgpsig}}:"
> +                       "{%|RSAHEADER?{%{RSAHEADER:pgpsig}}:"
> +                       "{%|SIGGPG?{%{SIGGPG:pgpsig}}:"
> +                       "{%|SIGPGP?{%{SIGPGP:pgpsig}}:"
> +                       "{(none)}|}|}|}|").split()[-1]
> +
>  class RPMPackageManager(PackageManager):
>
>      def commit(self, changeset, pkgpaths):
> @@ -152,12 +164,53 @@ class RPMPackageManager(PackageManager):
>                  info = loader.getInfo(pkg)
>                  mode = pkg in upgrading and "u" or "i"
>                  path = pkgpaths[pkg][0]
> -                fd = os.open(path, os.O_RDONLY)
> -                try:
> -                    h = ts.hdrFromFdno(fd)
> -                except rpm.error, e:
> -                    os.close(fd)
> -                    raise Error, "%s: %s" % (os.path.basename 
> (path), e)
> +                for _pass in xrange(2):
> +                    fd = os.open(path, os.O_RDONLY)
> +                    try:
> +                        h = ts.hdrFromFdno(fd)
> +                    except rpm.error, e:
> +                        os.close(fd)
> +
> +                        # in the slow path we trap the case of a  
> gpg public key not in rpmdb
> +                        # and we try to fetch it from the  
> keyserver and we ask the user
> +                        # if he wants to trust this key to  
> continue the installation
> +                        if not _pass and e.args[0] == 'public key  
> not available':
> +                            keyserver = sysconf.get('keyserver')
> +                            if not keyserver:
> +                                iface.error('To enable the  
> keyserver run: '
> +                                            '"smart config --set  
> keyserver=pgp.mit.edu"')
> +                            else:
> +                                iface.info('We are importing an  
> unknown key please wait...')
> +                                key = get_rpm_public_key(path)
> +                                popen = popen2.Popen4('gpg -- 
> keyserver %s --recv-keys %s' % (keyserver, key))
> +                                popen_out = popen.fromchild.read()
> +                                if popen.wait():
> +                                    iface.error('gpg failed to  
> import keyid %s, '
> +                                                'please make sure  
> that gpg is installed, '
> +                                                'that the  
> keyserver %s is working '
> +                                                'and that the  
> package %s has a valid signature.' % (key,
> +                                                                      
>                                keyserver,
> +                                                                      
>                                path))
> +                                else:
> +                                    popen_out += '\nThe above GPG  
> key has been imported successfully.\n' \
> +                                                 'It is required  
> to install this package:\n\n\t' \
> +                                                 + os.path.basename 
> (path) + \
> +                                                 '\n\nAre you sure  
> that you want to trust this key forever?\n\n' \
> +                                                 'You must verify  
> the below fingerprint before answering.\n' \
> +                                                 + os.popen('gpg -- 
> fingerprint %s' % key).read() + \
> +                                                 '\nIf you answer  
> "Yes" all other packages signed with this key ' \
> +                                                 'will be  
> installed automatically.'
> +                                    if iface.askYesNo(popen_out):
> +                                        key_armor = os.popen('gpg  
> --export %s' % key, 'r').read()
> +                                        if ts.pgpImportPubkey 
> (key_armor):
> +                                            iface.error('rpm  
> failed to import the public key id %s' % key)
> +                                        else:
> +                                            # try one more time  
> with the pub key on
> +                                            continue
> +
> +                        raise Error, "%s: %s" % (os.path.basename 
> (path), e)
> +                    else:
> +                        break
>                  os.close(fd)
>                  ts.addInstall(h, (info, path), mode)
>                  packages += 1
> Index: smart/backends/rpm/base.py
> --- smart/backends/rpm/base.py.~1~	2006-06-08 05:31:35.000000000 +0200
> +++ smart/backends/rpm/base.py	2006-06-15 19:56:18.000000000 +0200
> @@ -47,7 +47,7 @@ def getTS(new=False):
>      if not hasattr(getTS, "ts"):
>          getTS.root = sysconf.get("rpm-root", "/")
>          getTS.ts = rpm.ts(getTS.root)
> -        if not sysconf.get("rpm-check-signatures", False):
> +        if not sysconf.get("rpm-check-signatures", True):
>              getTS.ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
>          dbdir = os.path.join(getTS.root, "var/lib/rpm")
>          if not os.path.isdir(dbdir):
> @@ -73,7 +73,7 @@ def getTS(new=False):
>                  pass
>      if new:
>          ts = rpm.ts(getTS.root)
> -        if not sysconf.get("rpm-check-signatures", False):
> +        if not sysconf.get("rpm-check-signatures", True):
>              ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
>          return ts
>      else:
> Index: smart/interface.py
> --- smart/interface.py.~1~	2006-06-08 05:31:38.000000000 +0200
> +++ smart/interface.py	2006-06-15 20:37:32.000000000 +0200
> @@ -70,13 +70,13 @@ class Interface(object):
>          return self._progress
>
>      def askYesNo(self, question, default=False):
> -        return True
> +        return default
>
>      def askContCancel(self, question, default=False):
> -        return True
> +        return default
>
>      def askOkCancel(self, question, default=False):
> -        return True
> +        return default
>
>      def askInput(self, prompt, message=None, widthchars=None,  
> echo=True):
>          return ""




More information about the Smart mailing list