key: flesh out to use KEYLIST's output XML.
authorW. Trevor King <wking@tremily.us>
Sat, 6 Oct 2012 14:47:52 +0000 (10:47 -0400)
committerW. Trevor King <wking@tremily.us>
Sat, 6 Oct 2012 15:09:22 +0000 (11:09 -0400)
Since GPGME commit

  commit 3d69b51f7f083193db28f364da4590c33b5e44e6
  Author: W. Trevor King <wking@tremily.us>
  Date:   Fri Sep 28 08:31:13 2012 +0200

    gpgme-tool: Return more detailed XML from KEYLIST

and my two subsequent patches (not yet accepted upstream)

  gpgme-tool: Fix chain_id -> chain-id in KEYLIST XML. [1]
  gpgme-tool: escape special characters in output XML data
    (<, >, and &). [2]

The new KEYLIST output looks like this:

  <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  <gpgme>
    <keylist>
      <key>
        <revoked value="0x0" />
        <expired value="0x0" />
        <disabled value="0x0" />
        <invalid value="0x0" />
        <can-encrypt value="0x1" />
        <can-sign value="0x1" />
        <can-certify value="0x1" />
        <can-authenticate value="0x0" />
        <is-qualified value="0x0" />
        <secret value="0x0" />
        <protocol value="0">OpenPGP</protocol>
        <issuer>
          <serial></serial>
          <name></name>
        </issuer>
        <chain-id></chain-id>
        <owner-trust value="0">unknown</owner-trust>
        <subkeys>
          <subkey>
            <fpr>B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3</fpr>
          </subkey>
          <subkey>
            <fpr>DECC812C8795ADD60538B0CD171008BA2F73DE2E</fpr>
          </subkey>
        </subkeys>
        <uids>
          <uid>
            <uid>pgp-mime-test (http://blog.tremily.us/posts/pgp-mime/) &lt;pgp-mime@invalid.com&gt;</uid>
            <name>pgp-mime-test</name>
            <email>pgp-mime@invalid.com</email>
            <comment>http://blog.tremily.us/posts/pgp-mime/</comment>
          </uid>
        </uids>
      </key>
    </keylist>
  </gpgme>

[1]: http://article.gmane.org/gmane.comp.encryption.gpg.devel/17151
[2]: http://article.gmane.org/gmane.comp.encryption.gpg.devel/17152

Signed-off-by: W. Trevor King <wking@tremily.us>
pgp_mime/key.py

index aa735bf0c8f281591a9867482314cb7b1ec2fde9..7aa1c8a2310044700708a5501e35faa09ec5cf8a 100644 (file)
@@ -1,5 +1,6 @@
 # Copyright
 
+import functools as _functools
 import xml.etree.ElementTree as _etree
 
 from . import LOG as _LOG
@@ -10,21 +11,125 @@ import logging
 from pyassuan import common as _common
 
 
-class Key (object):
+@_functools.total_ordering
+class SubKey (object):
+    """The crypographic key portion of an OpenPGP key.
+    """
     def __init__(self, fingerprint=None):
         self.fingerprint = fingerprint
-        # more data to come once gpgme-tool gets keylist XML support
 
     def __str__(self):
-        return '<{} {}>'.format(self.__class__.__name__, self.fingerprint[-8:])
+        return '<{} {}>'.format(type(self).__name__, self.fingerprint[-8:])
+
+    def __repr__(self):
+        return str(self)
+
+    def __eq__(self, other):
+        if self.fingerprint and hasattr(other, 'fingerprint'):
+            return self.fingerprint == other.fingerprint
+        return id(self) == id(other)
+
+    def __lt__(self, other):
+        if self.fingerprint and hasattr(other, 'fingerprint'):
+            return self.fingerprint < other.fingerprint
+        return id(self) < id(other)
+
+    def __hash__(self):
+        return int(self.fingerprint, 16)
+
+
+@_functools.total_ordering
+class UserID (object):
+    def __init__(self, uid=None, name=None, email=None, comment=None):
+        self.uid = uid
+        self.name = name
+        self.email = email
+        self.comment = comment
+
+    def __str__(self):
+        return '<{} {}>'.format(type(self).__name__, self.name)
+
+    def __repr__(self):
+        return str(self)
+
+    def __eq__(self, other):
+        if self.uid and hasattr(other, 'uid'):
+            return self.uid == other.uid
+        return id(self) == id(other)
+
+    def __lt__(self, other):
+        if self.uid and hasattr(other, 'uid'):
+            return self.uid < other.uid
+        return id(self) < id(other)
+
+    def __hash__(self):
+        return hash(self.uid)
+
+
+@_functools.total_ordering
+class Key (object):
+    def __init__(self, subkeys=None, uids=None):
+        revoked = False
+        expired = False
+        disabled = False
+        invalid = False
+        can_encrypt = False
+        can_sign = False
+        can_certify = False
+        can_authenticate = False
+        is_qualified = False
+        secret = False
+        protocol = None
+        issuer = None
+        chain_id = None
+        owner_trust = None
+        if subkeys is None:
+            subkeys = []
+        self.subkeys = subkeys
+        if uids is None:
+            uids = []
+        self.uids = uids
+
+    def __str__(self):
+        return '<{} {}>'.format(
+            type(self).__name__, self.subkeys[0].fingerprint[-8:])
 
     def __repr__(self):
         return str(self)
 
+    def __eq__(self, other):
+        other_subkeys = getattr(other, 'subkeys', None)
+        if self.subkeys and other_subkeys:
+            return self.subkeys[0] == other.subkeys[0]
+        return id(self) == id(other)
+
+    def __lt__(self, other):
+        other_subkeys = getattr(other, 'subkeys', None)
+        if self.subkeys and other_subkeys:
+            return self.subkeys[0] < other.subkeys[0]
+        return id(self) < id(other)
 
-def lookup_keys(patterns=None, load=False):
+    def __hash__(self):
+        return int(self.fingerprint, 16)
+
+
+def lookup_keys(patterns=None):
     """Lookup keys matching any patterns listed in ``patterns``.
 
+    >>> import pprint
+
+    >>> key = list(lookup_keys(['pgp-mime-test']))[0]
+    >>> key
+    <Key 4332B6E3>
+    >>> key.subkeys
+    [<SubKey 4332B6E3>, <SubKey 2F73DE2E>]
+    >>> key.uids
+    [<UserID pgp-mime-test>]
+    >>> key.can_encrypt
+    True
+    >>> key.protocol
+    'OpenPGP'
+
     >>> print(list(lookup_keys(['pgp-mime-test'])))
     [<Key 4332B6E3>]
     >>> print(list(lookup_keys(['pgp-mime@invalid.com'])))
@@ -35,15 +140,8 @@ def lookup_keys(patterns=None, load=False):
     [<Key 4332B6E3>]
     >>> print(list(lookup_keys()))  # doctest: +ELLIPSIS
     [..., <Key 4332B6E3>, ...]
-
-    >>> key = lookup_keys(['2F73DE2E'], load=True)
-    >>> print(list(key)[0])
-    Traceback (most recent call last):
-      ...
-    NotImplementedError: gpgme-tool doesn't return keylist data
     """
     _LOG.debug('lookup key: {}'.format(patterns))
-    pyassuan.LOG.setLevel(logging.DEBUG)
     client,socket = _crypt.get_client()
     parameters = []
     if patterns:
@@ -52,36 +150,79 @@ def lookup_keys(patterns=None, load=False):
         args = []
     try:
         _crypt.hello(client)
-        if load:
-            client.make_request(_common.Request('KEYLIST', *args))
-            rs,result = client.make_request(_common.Request('RESULT'))
-        else:
-            rs,result = client.make_request(_common.Request('KEYLIST', *args))
+        rs,result = client.make_request(_common.Request('KEYLIST', *args))
     finally:
         _crypt.disconnect(client, socket)
-    if load:
-        tag_mapping = {
-            'fpr': 'fingerprint',
-            }
-        tree = _etree.fromstring(result.replace(b'\x00', b''))
-        if list(tree.findall('.//truncated')):
-            raise NotImplementedError("gpgme-tool doesn't return keylist data")
-        for signature in tree.findall('.//key'):
-            key = Key()
-            for child in signature.iter():
-                if child == signature:  # iter() includes the root element
-                    continue
-                attribute = tag_mapping.get(
-                    child.tag, child.tag.replace('-', '_'))
-                if child.tag in ['fpr']:
-                    value = child.text
-                else:
-                    raise NotImplementedError(child.tag)
-                setattr(s, attribute, value)
-            yield key
-    else:
-        for line in result.splitlines():
-            line = str(line, 'ascii')
-            assert line.startswith('key:'), result
-            fingerprint = line.split(':', 1)[1]
-            yield Key(fingerprint=fingerprint)
+    tag_mapping = {
+        }
+    tree = _etree.fromstring(result.replace(b'\x00', b''))
+    for key in tree.findall('.//key'):
+        k = Key()
+        for child in key:
+            attribute = tag_mapping.get(
+                child.tag, child.tag.replace('-', '_'))
+            if child.tag in [
+                'revoked', 'expired', 'disabled', 'invalid', 'can-encrypt',
+                'can-sign', 'can-certify', 'can-authenticate', 'is-qualified',
+                'secret', 'revoked']:
+                # boolean values
+                value = child.get('value')
+                if not value.startswith('0x'):
+                    raise NotImplementedError('{} value {}'.format(
+                            child.tag, value))
+                value = int(value, 16)
+                value = bool(value)
+            elif child.tag in [
+                'protocol', 'owner-trust']:
+                value = child.text
+            elif child.tag in ['issuer', 'chain-id']:
+                # ignore for now
+                pass
+            elif child.tag in ['subkeys', 'uids']:
+                parser = globals()['_parse_{}'.format(attribute)]
+                value = parser(child)
+            else:
+                raise NotImplementedError(child.tag)
+            setattr(k, attribute, value)
+        yield k
+
+def _parse_subkeys(element):
+    tag_mapping = {
+        'fpr': 'fingerprint',
+        }
+    subkeys = []
+    for subkey in element:
+        s = SubKey()
+        for child in subkey.iter():
+            if child == subkey:  # iter() includes the root element
+                continue
+            attribute = tag_mapping.get(
+                child.tag, child.tag.replace('-', '_'))
+            if child.tag in [
+                'fpr']:
+                value = child.text
+            else:
+                raise NotImplementedError(child.tag)
+            setattr(s, attribute, value)
+        subkeys.append(s)
+    return subkeys
+
+def _parse_uids(element):
+    tag_mapping = {
+        }
+    uids = []
+    for uid in element:
+        u = UserID()
+        for child in uid.iter():
+            if child == uid:  # iter() includes the root element
+                continue
+            attribute = tag_mapping.get(
+                child.tag, child.tag.replace('-', '_'))
+            if child.tag in [
+                'uid', 'name', 'email', 'comment']:
+                value = child.text
+            else:
+                raise NotImplementedError(child.tag)
+            setattr(u, attribute, value)
+        uids.append(u)
+    return uids