signature: use proper tag in failed-integer-value-conversion error message.
[pgp-mime.git] / pgp_mime / signature.py
1 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of pgp-mime.
4 #
5 # pgp-mime is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # pgp-mime is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # pgp-mime.  If not, see <http://www.gnu.org/licenses/>.
16
17 """A Python version of GPGME verification signatures.
18
19 See the `GPGME manual`_ for details.
20
21 .. GPGME manual: http://www.gnupg.org/documentation/manuals/gpgme/Verify.html
22 """
23
24 import time as _time
25 import xml.etree.ElementTree as _etree
26
27
28 class Signature (object):
29     """Python version of ``gpgme_signature_t``
30
31     >>> from pprint import pprint
32     >>> s = Signature()
33
34     You can set flag fields using their integer value (from C).
35
36     >>> s.set_summary(0x3)
37
38     This sets up a convenient dictionary.
39
40     >>> pprint(s.summary)
41     {'CRL missing': False,
42      'CRL too old': False,
43      'bad policy': False,
44      'green': True,
45      'key expired': False,
46      'key missing': False,
47      'key revoked': False,
48      'red': False,
49      'signature expired': False,
50      'system error': False,
51      'valid': True}
52
53     If you alter the dictionary, it's easy to convert back to the
54     equivalent integer value.
55
56     >>> s.summary['green'] = s.summary['valid'] = False
57     >>> s.summary['red'] = s.summary['key expired'] = True
58     >>> type(s.get_summary())
59     <class 'int'>
60     >>> '0x{:x}'.format(s.get_summary())
61     '0x24'
62
63     If you try and parse a flag field, but have some wonky input, you
64     get a helpful error.
65
66     >>> s.set_summary(-1)
67     Traceback (most recent call last):
68       ...
69     ValueError: invalid flags for summary (-1)
70     >>> s.set_summary(0x1024)
71     Traceback (most recent call last):
72       ...
73     ValueError: unknown flags for summary (0x1000)
74
75     You can set enumerated fields using their integer value.
76
77     >>> s.set_status(94)
78     >>> s.status
79     'certificate revoked'
80     >>> s.status = 'bad signature'
81     >>> s.get_status()
82     8
83
84     >>> s.fingerprint = 'ABCDEFG'
85     >>> print(s.dumps())  # doctest: +REPORT_UDIFF
86     ABCDEFG signature:
87       summary:
88         CRL missing: False
89         CRL too old: False
90         bad policy: False
91         green: False
92         key expired: True
93         key missing: False
94         key revoked: False
95         red: True
96         signature expired: False
97         system error: False
98         valid: False
99       status: bad signature
100     >>> print(s.dumps(prefix='xx'))  # doctest: +REPORT_UDIFF
101     xxABCDEFG signature:
102     xx  summary:
103     xx    CRL missing: False
104     xx    CRL too old: False
105     xx    bad policy: False
106     xx    green: False
107     xx    key expired: True
108     xx    key missing: False
109     xx    key revoked: False
110     xx    red: True
111     xx    signature expired: False
112     xx    system error: False
113     xx    valid: False
114     xx  status: bad signature
115     """
116     _error_enum = {  # GPG_ERR_* in gpg-error.h
117         0: 'success',
118         1: 'general error',
119         8: 'bad signature',
120         9: 'no public key',
121         94: 'certificate revoked',
122         153: 'key expired',
123         154: 'signature expired',
124         # lots more, to be included as they occur in the wild
125         }
126     _error_enum_inv = dict((v,k) for k,v in _error_enum.items())
127
128     _summary_flags = {  # GPGME_SIGSUM_* in gpgme.h
129         0x001: 'valid',
130         0x002: 'green',
131         0x004: 'red',
132         0x008: 'key revoked',
133         0x020: 'key expired',
134         0x040: 'signature expired',
135         0x080: 'key missing',
136         0x100: 'CRL missing',
137         0x200: 'CRL too old',
138         0x400: 'bad policy',
139         0x800: 'system error',
140         }
141
142     _pka_trust_enum = {  # struct _gpgme_signature in gpgme.h
143         0: 'not available',
144         1: 'bad',
145         2: 'good',
146         3: 'reserved',
147         }
148     _pka_trust_enum_inv = dict((v,k) for k,v in _pka_trust_enum.items())
149
150     _validity_enum = {  # GPGME_VALIDITY_* in gpgme.h
151         0: 'unknown',
152         1: 'undefined',
153         2: 'never',
154         3: 'marginal',
155         4: 'full',
156         5: 'ultimate',
157         }
158     _validity_enum_inv = dict((v,k) for k,v in _validity_enum.items())
159
160     _public_key_algorithm_enum = {  # GPGME_PK_* in gpgme.h
161         0: 'none',
162         1: 'RSA',      # Rivest, Shamir, Adleman
163         2: 'RSA for encryption and decryption only',
164         3: 'RSA for signing and verification only',
165         16: 'ELGamal in GnuPG',
166         17: 'DSA',     # Digital Signature Algorithm
167         20: 'ELGamal',
168         301: 'ECDSA',  # Elliptic Curve Digital Signature Algorithm
169         302: 'ECDH',   # Elliptic curve Diffie-Hellman
170         }
171     _public_key_algorithm_enum_inv = dict(
172         (v,k) for k,v in _public_key_algorithm_enum.items())
173
174     _hash_algorithm_enum = {  # GPGME_MD_* in gpgme.h
175         0: 'none',
176         1: 'MD5',
177         2: 'SHA1',
178         3: 'RMD160',
179         5: 'MD2',
180         6: 'TIGER/192',
181         7: 'HAVAL, 5 pass, 160 bit',
182         8: 'SHA256',
183         9: 'SHA384',
184         10: 'SHA512',
185         301: 'MD4',
186         302: 'CRC32',
187         303: 'CRC32 RFC1510',
188         304: 'CRC24 RFC2440',
189         }
190     _hash_algorithm_enum_inv = dict(
191         (v,k) for k,v in _hash_algorithm_enum.items())
192
193     def __init__(self, summary=None, fingerprint=None, status=None,
194                  notations=None, timestamp=None, expiration_timestamp=None,
195                  wrong_key_usage=None, pka_trust=None, chain_model=None,
196                  validity=None, validity_reason=None,
197                  public_key_algorithm=None, hash_algorithm=None):
198         self.summary = summary
199         self.fingerprint = fingerprint
200         self.status = status
201         self.notations = notations
202         self.timestamp = timestamp
203         self.expiration_timestamp = expiration_timestamp
204         self.wrong_key_usage = wrong_key_usage
205         self.pka_trust = pka_trust
206         self.chain_model = chain_model
207         self.validity = validity
208         self.validity_reason = validity_reason
209         self.public_key_algorithm = public_key_algorithm
210         self.hash_algorithm = hash_algorithm
211
212     def _set_flags(self, attribute, value, flags):
213         if value < 0:
214             raise ValueError(
215                 'invalid flags for {} ({})'.format(attribute, value))
216         d = {}
217         for flag,name in flags.items():
218             x = flag & value
219             d[name] = bool(x)
220             value -= x
221         if value:
222             raise ValueError(
223                 'unknown flags for {} (0x{:x})'.format(attribute, value))
224         setattr(self, attribute, d)
225
226     def _get_flags(self, attribute, flags):
227         value = 0
228         d = getattr(self, attribute)
229         for flag,name in flags.items():
230             if d[name]:
231                 value |= flag
232         return value
233
234     def set_summary(self, value):
235         self._set_flags('summary', value, self._summary_flags)
236
237     def get_summary(self):
238         return self._get_flags('summary', self._summary_flags)
239
240     def set_status(self, value):
241         self.status = self._error_enum[value]
242
243     def get_status(self):
244         return self._error_enum_inv[self.status]
245
246     def set_pka_trust(self, value):
247         self.pka_trust = self._pka_trust_enum[value]
248
249     def get_pka_trust(self):
250         return self._pka_trust_enum_inv[self.pka_trust]
251
252     def set_validity(self, value):
253         self.validity = self._validity_enum[value]
254
255     def get_validity(self):
256         return self._error_validity_inv[self.validity]
257
258     def set_validity_reason(self, value):
259         self.validity_reason = self._error_enum[value]
260
261     def get_validity_reason(self):
262         return self._error_enum_inv[self.validity_reason]
263
264     def set_public_key_algorithm(self, value):
265         self.public_key_algorithm = self._public_key_algorithm_enum[value]
266
267     def get_public_key_algorithm(self):
268         return self._public_key_algorithm_inv[self.public_key_algorithm]
269
270     def set_hash_algorithm(self, value):
271         self.hash_algorithm = self._hash_algorithm_enum[value]
272
273     def get_hash_algorithm(self):
274         return self._error_hash_algorithm_inv[self.hash_algorithm]
275
276     def dumps(self, prefix=''):
277         lines = ['{}{} signature:'.format(prefix, self.fingerprint)]
278         for attribute in ['summary', 'status', 'notations', 'timestamp',
279                           'expiration_timestamp', 'wrong_key_usage',
280                           'pka_trust', 'chain_model', 'validity',
281                           'validity_reason', 'public_key_algorithm',
282                           'hash_algorithm']:
283             label = attribute.replace('_', ' ')
284             value = getattr(self, attribute)
285             if value is None:
286                 continue  # no information
287             elif attribute.endswith('timestamp'):
288                 if value == 0 and attribute == 'expiration_timestamp':
289                     value = None
290                 else:
291                     value = _time.asctime(_time.gmtime(value))
292             if isinstance(value, dict):  # flag field
293                 lines.append('  {}:'.format(label))
294                 lines.extend(
295                     ['    {}: {}'.format(k,v)
296                      for k,v in sorted(value.items())])
297             else:
298                 lines.append('  {}: {}'.format(label, value))
299         sep = '\n{}'.format(prefix)
300         return sep.join(lines)
301
302
303 def verify_result_signatures(result):
304     """
305     >>> from pprint import pprint
306     >>> result = b'\\n'.join([
307     ...     b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
308     ...     b'<gpgme>',
309     ...     b'  <verify-result>',
310     ...     b'    <signatures>',
311     ...     b'      <signature>',
312     ...     b'        <summary value="0x0" />',
313     ...     b'        <fpr>B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3</fpr>',
314     ...     b'        <status value="0x0">Success &lt;Unspecified source&gt;</status>',
315     ...     b'        <timestamp unix="1332358207i" />',
316     ...     b'        <exp-timestamp unix="0i" />',
317     ...     b'        <wrong-key-usage value="0x0" />',
318     ...     b'        <pka-trust value="0x0" />',
319     ...     b'        <chain-model value="0x0" />',
320     ...     b'        <validity value="0x0" />',
321     ...     b'        <validity-reason value="0x0">Success &lt;Unspecified source&gt;</validity-reason>',
322     ...     b'        <pubkey-algo value="0x1">RSA</pubkey-algo>',
323     ...     b'        <hash-algo value="0x2">SHA1</hash-algo>',
324     ...     b'      </signature>',
325     ...     b'    </signatures>',
326     ...     b'  </verify-result>',
327     ...     b'</gpgme>',
328     ...     b'',
329     ...     ])
330     >>> signatures = list(verify_result_signatures(result))
331     >>> signatures  # doctest: +ELLIPSIS
332     [<pgp_mime.signature.Signature object at 0x...>]
333     >>> for s in signatures:
334     ...     print(s.dumps())  # doctest: +REPORT_UDIFF
335     B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3 signature:
336       summary:
337         CRL missing: False
338         CRL too old: False
339         bad policy: False
340         green: False
341         key expired: False
342         key missing: False
343         key revoked: False
344         red: False
345         signature expired: False
346         system error: False
347         valid: False
348       status: success
349       timestamp: Wed Mar 21 19:30:07 2012
350       expiration timestamp: None
351       wrong key usage: False
352       pka trust: not available
353       chain model: False
354       validity: unknown
355       validity reason: success
356       public key algorithm: RSA
357       hash algorithm: SHA1
358     """
359     tag_mapping = {
360         'exp-timestamp': 'expiration_timestamp',
361         'fpr': 'fingerprint',
362         'pubkey-algo': 'public_key_algorithm',
363         'hash-algo': 'hash_algorithm',
364         }
365     tree = _etree.fromstring(result.replace(b'\x00', b''))
366     for signature in tree.findall('.//signature'):
367         s = Signature()
368         for child in signature.iter():
369             if child == signature:  # iter() includes the root element
370                 continue
371             attribute = tag_mapping.get(child.tag, child.tag.replace('-', '_'))
372             if child.tag in ['summary', 'wrong-key-usage', 'pka-trust',
373                              'chain-model', 'validity', 'pubkey-algo',
374                              'hash-algo']:
375                 value = child.get('value')
376                 if not value.startswith('0x'):
377                     raise NotImplementedError('{} value {}'.format(
378                             child.tag, value))
379                 value = int(value, 16)
380                 if attribute in ['wrong_key_usage', 'chain_model']:
381                     value = bool(value)  # boolean
382                 else:  # flags or enum
383                     setter = getattr(s, 'set_{}'.format(attribute))
384                     setter(value)
385                     continue
386             elif child.tag in ['timestamp', 'exp-timestamp']:
387                 value = child.get('unix')
388                 if value.endswith('i'):
389                     value = int(value[:-1])
390                 else:
391                     raise NotImplementedError('timestamp value {}'.format(value))
392             elif child.tag in ['fpr', 'status', 'validity-reason']:
393                 value = child.text
394                 if value.endswith(' <Unspecified source>'):
395                     value = value[:-len(' <Unspecified source>')].lower()
396             else:
397                 raise NotImplementedError(child.tag)
398             setattr(s, attribute, value)
399         yield s