1 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
3 # This file is part of pgp-mime.
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
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.
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/>.
17 """A Python version of GPGME verification signatures.
19 See the `GPGME manual`_ for details.
21 .. GPGME manual: http://www.gnupg.org/documentation/manuals/gpgme/Verify.html
25 import xml.etree.ElementTree as _etree
28 class Signature (object):
29 """Python version of ``gpgme_signature_t``
31 >>> from pprint import pprint
34 You can set flag fields using their integer value (from C).
36 >>> s.set_summary(0x3)
38 This sets up a convenient dictionary.
41 {'CRL missing': False,
49 'signature expired': False,
50 'system error': False,
53 If you alter the dictionary, it's easy to convert back to the
54 equivalent integer value.
56 >>> s.summary['green'] = s.summary['valid'] = False
57 >>> s.summary['red'] = s.summary['key expired'] = True
58 >>> type(s.get_summary())
60 >>> '0x{:x}'.format(s.get_summary())
63 If you try and parse a flag field, but have some wonky input, you
67 Traceback (most recent call last):
69 ValueError: invalid flags for summary (-1)
70 >>> s.set_summary(0x1024)
71 Traceback (most recent call last):
73 ValueError: unknown flags for summary (0x1000)
75 You can set enumerated fields using their integer value.
80 >>> s.status = 'bad signature'
84 >>> s.fingerprint = 'ABCDEFG'
85 >>> print(s.dumps()) # doctest: +REPORT_UDIFF
96 signature expired: False
100 >>> print(s.dumps(prefix='xx')) # doctest: +REPORT_UDIFF
103 xx CRL missing: False
104 xx CRL too old: False
108 xx key missing: False
109 xx key revoked: False
111 xx signature expired: False
112 xx system error: False
114 xx status: bad signature
116 _error_enum = { # GPG_ERR_* in gpg-error.h
121 94: 'certificate revoked',
123 154: 'signature expired',
124 # lots more, to be included as they occur in the wild
126 _error_enum_inv = dict((v,k) for k,v in _error_enum.items())
128 _summary_flags = { # GPGME_SIGSUM_* in gpgme.h
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',
139 0x800: 'system error',
142 _pka_trust_enum = { # struct _gpgme_signature in gpgme.h
148 _pka_trust_enum_inv = dict((v,k) for k,v in _pka_trust_enum.items())
150 _validity_enum = { # GPGME_VALIDITY_* in gpgme.h
158 _validity_enum_inv = dict((v,k) for k,v in _validity_enum.items())
160 _public_key_algorithm_enum = { # GPGME_PK_* in gpgme.h
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
168 301: 'ECDSA', # Elliptic Curve Digital Signature Algorithm
169 302: 'ECDH', # Elliptic curve Diffie-Hellman
171 _public_key_algorithm_enum_inv = dict(
172 (v,k) for k,v in _public_key_algorithm_enum.items())
174 _hash_algorithm_enum = { # GPGME_MD_* in gpgme.h
181 7: 'HAVAL, 5 pass, 160 bit',
187 303: 'CRC32 RFC1510',
188 304: 'CRC24 RFC2440',
190 _hash_algorithm_enum_inv = dict(
191 (v,k) for k,v in _hash_algorithm_enum.items())
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
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
212 def _set_flags(self, attribute, value, flags):
215 'invalid flags for {} ({})'.format(attribute, value))
217 for flag,name in flags.items():
223 'unknown flags for {} (0x{:x})'.format(attribute, value))
224 setattr(self, attribute, d)
226 def _get_flags(self, attribute, flags):
228 d = getattr(self, attribute)
229 for flag,name in flags.items():
234 def set_summary(self, value):
235 self._set_flags('summary', value, self._summary_flags)
237 def get_summary(self):
238 return self._get_flags('summary', self._summary_flags)
240 def set_status(self, value):
241 self.status = self._error_enum[value]
243 def get_status(self):
244 return self._error_enum_inv[self.status]
246 def set_pka_trust(self, value):
247 self.pka_trust = self._pka_trust_enum[value]
249 def get_pka_trust(self):
250 return self._pka_trust_enum_inv[self.pka_trust]
252 def set_validity(self, value):
253 self.validity = self._validity_enum[value]
255 def get_validity(self):
256 return self._error_validity_inv[self.validity]
258 def set_validity_reason(self, value):
259 self.validity_reason = self._error_enum[value]
261 def get_validity_reason(self):
262 return self._error_enum_inv[self.validity_reason]
264 def set_public_key_algorithm(self, value):
265 self.public_key_algorithm = self._public_key_algorithm_enum[value]
267 def get_public_key_algorithm(self):
268 return self._public_key_algorithm_inv[self.public_key_algorithm]
270 def set_hash_algorithm(self, value):
271 self.hash_algorithm = self._hash_algorithm_enum[value]
273 def get_hash_algorithm(self):
274 return self._error_hash_algorithm_inv[self.hash_algorithm]
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',
283 label = attribute.replace('_', ' ')
284 value = getattr(self, attribute)
286 continue # no information
287 elif attribute.endswith('timestamp'):
288 if value == 0 and attribute == 'expiration_timestamp':
291 value = _time.asctime(_time.gmtime(value))
292 if isinstance(value, dict): # flag field
293 lines.append(' {}:'.format(label))
295 [' {}: {}'.format(k,v)
296 for k,v in sorted(value.items())])
298 lines.append(' {}: {}'.format(label, value))
299 sep = '\n{}'.format(prefix)
300 return sep.join(lines)
303 def verify_result_signatures(result):
305 >>> from pprint import pprint
306 >>> result = b'\\n'.join([
307 ... b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
309 ... b' <verify-result>',
310 ... b' <signatures>',
312 ... b' <summary value="0x0" />',
313 ... b' <fpr>B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3</fpr>',
314 ... b' <status value="0x0">Success <Unspecified source></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 <Unspecified source></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>',
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:
345 signature expired: False
349 timestamp: Wed Mar 21 19:30:07 2012
350 expiration timestamp: None
351 wrong key usage: False
352 pka trust: not available
355 validity reason: success
356 public key algorithm: RSA
360 'exp-timestamp': 'expiration_timestamp',
361 'fpr': 'fingerprint',
362 'pubkey-algo': 'public_key_algorithm',
363 'hash-algo': 'hash_algorithm',
365 tree = _etree.fromstring(result.replace(b'\x00', b''))
366 for signature in tree.findall('.//signature'):
368 for child in signature.iter():
369 if child == signature: # iter() includes the root element
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',
375 value = child.get('value')
376 if not value.startswith('0x'):
377 raise NotImplementedError('summary value {}'.format(value))
378 value = int(value, 16)
379 if attribute in ['wrong_key_usage', 'chain_model']:
380 value = bool(value) # boolean
381 else: # flags or enum
382 setter = getattr(s, 'set_{}'.format(attribute))
385 elif child.tag in ['timestamp', 'exp-timestamp']:
386 value = child.get('unix')
387 if value.endswith('i'):
388 value = int(value[:-1])
390 raise NotImplementedError('timestamp value {}'.format(value))
391 elif child.tag in ['fpr', 'status', 'validity-reason']:
393 if value.endswith(' <Unspecified source>'):
394 value = value[:-len(' <Unspecified source>')].lower()
396 raise NotImplementedError(child.tag)
397 setattr(s, attribute, value)