52655d0e83a12ef8abdabdd309ac64796a29f201
[keymanager.git] / src / leap / keymanager / openpgp.py
1 # -*- coding: utf-8 -*-
2 # openpgp.py
3 # Copyright (C) 2013 LEAP
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 """
18 Infrastructure for using OpenPGP keys in Key Manager.
19 """
20 import logging
21 import os
22 import re
23 import shutil
24 import tempfile
25 import io
26
27
28 from datetime import datetime
29 from gnupg import GPG
30 from gnupg.gnupg import GPGUtilities
31
32 from leap.common.check import leap_assert, leap_assert_type, leap_check
33 from leap.keymanager import errors
34 from leap.keymanager.keys import (
35     EncryptionKey,
36     EncryptionScheme,
37     is_address,
38     build_key_from_dict,
39     TYPE_ID_PRIVATE_INDEX,
40     TYPE_ADDRESS_PRIVATE_INDEX,
41     KEY_FINGERPRINT_KEY,
42     KEY_DATA_KEY,
43     KEY_ID_KEY,
44     KEYMANAGER_ACTIVE_TYPE,
45 )
46
47
48 logger = logging.getLogger(__name__)
49
50
51 #
52 # A temporary GPG keyring wrapped to provide OpenPGP functionality.
53 #
54
55 class TempGPGWrapper(object):
56     """
57     A context manager that wraps a temporary GPG keyring which only contains
58     the keys given at object creation.
59     """
60
61     def __init__(self, keys=None, gpgbinary=None):
62         """
63         Create an empty temporary keyring and import any given C{keys} into
64         it.
65
66         :param keys: OpenPGP key, or list of.
67         :type keys: OpenPGPKey or list of OpenPGPKeys
68         :param gpgbinary: Name for GnuPG binary executable.
69         :type gpgbinary: C{str}
70         """
71         self._gpg = None
72         self._gpgbinary = gpgbinary
73         if not keys:
74             keys = list()
75         if not isinstance(keys, list):
76             keys = [keys]
77         self._keys = keys
78         for key in keys:
79             leap_assert_type(key, OpenPGPKey)
80
81     def __enter__(self):
82         """
83         Build and return a GPG keyring containing the keys given on
84         object creation.
85
86         :return: A GPG instance containing the keys given on object creation.
87         :rtype: gnupg.GPG
88         """
89         self._build_keyring()
90         return self._gpg
91
92     def __exit__(self, exc_type, exc_value, traceback):
93         """
94         Ensure the gpg is properly destroyed.
95         """
96         # TODO handle exceptions and log here
97         self._destroy_keyring()
98
99     def _build_keyring(self):
100         """
101         Create a GPG keyring containing the keys given on object creation.
102
103         :return: A GPG instance containing the keys given on object creation.
104         :rtype: gnupg.GPG
105         """
106         privkeys = [key for key in self._keys if key and key.private is True]
107         publkeys = [key for key in self._keys if key and key.private is False]
108         # here we filter out public keys that have a correspondent
109         # private key in the list because the private key_data by
110         # itself is enough to also have the public key in the keyring,
111         # and we want to count the keys afterwards.
112
113         privaddrs = map(lambda privkey: privkey.address[0], privkeys)
114         publkeys = filter(
115             lambda pubkey: pubkey.address[0] not in privaddrs, publkeys)
116
117         listkeys = lambda: self._gpg.list_keys()
118         listsecretkeys = lambda: self._gpg.list_keys(secret=True)
119
120         self._gpg = GPG(binary=self._gpgbinary,
121                         homedir=tempfile.mkdtemp())
122         leap_assert(len(listkeys()) is 0, 'Keyring not empty.')
123
124         # import keys into the keyring:
125         # concatenating ascii-armored keys, which is correctly
126         # understood by GPG.
127
128         self._gpg.import_keys("".join(
129             [x.key_data for x in publkeys + privkeys]))
130
131         # assert the number of keys in the keyring
132         leap_assert(
133             len(listkeys()) == len(publkeys) + len(privkeys),
134             'Wrong number of public keys in keyring: %d, should be %d)' %
135             (len(listkeys()), len(publkeys) + len(privkeys)))
136         leap_assert(
137             len(listsecretkeys()) == len(privkeys),
138             'Wrong number of private keys in keyring: %d, should be %d)' %
139             (len(listsecretkeys()), len(privkeys)))
140
141     def _destroy_keyring(self):
142         """
143         Securely erase the keyring.
144         """
145         # TODO: implement some kind of wiping of data or a more
146         # secure way that
147         # does not write to disk.
148
149         try:
150             for secret in [True, False]:
151                 for key in self._gpg.list_keys(secret=secret):
152                     self._gpg.delete_keys(
153                         key['fingerprint'],
154                         secret=secret)
155             leap_assert(len(self._gpg.list_keys()) is 0, 'Keyring not empty!')
156
157         except:
158             raise
159
160         finally:
161             leap_assert(self._gpg.homedir != os.path.expanduser('~/.gnupg'),
162                         "watch out! Tried to remove default gnupg home!")
163             shutil.rmtree(self._gpg.homedir)
164
165
166 def _build_key_from_gpg(address, key, key_data):
167     """
168     Build an OpenPGPKey for C{address} based on C{key} from
169     local gpg storage.
170
171     ASCII armored GPG key data has to be queried independently in this
172     wrapper, so we receive it in C{key_data}.
173
174     :param address: The address bound to the key.
175     :type address: str
176     :param key: Key obtained from GPG storage.
177     :type key: dict
178     :param key_data: Key data obtained from GPG storage.
179     :type key_data: str
180     :return: An instance of the key.
181     :rtype: OpenPGPKey
182     """
183     expiry_date = None
184     if key['expires']:
185         expiry_date = datetime.fromtimestamp(int(key['expires']))
186
187     return OpenPGPKey(
188         [address],
189         key_id=key['keyid'],
190         fingerprint=key['fingerprint'],
191         key_data=key_data,
192         private=True if key['type'] == 'sec' else False,
193         length=int(key['length']),
194         expiry_date=expiry_date,
195         refreshed_at=datetime.now(),
196     )
197
198
199 def _parse_address(address):
200     """
201     Remove the identity suffix after the '+' until the '@'
202     e.g.: test_user+something@provider.com becomes test_user@provider.com
203     since the key belongs to the identity without the '+' suffix.
204
205     :type address: str
206     :rtype: str
207     """
208     return re.sub(r'\+.*\@', '@', address)
209
210
211 #
212 # The OpenPGP wrapper
213 #
214
215 class OpenPGPKey(EncryptionKey):
216     """
217     Base class for OpenPGP keys.
218     """
219
220
221 class OpenPGPScheme(EncryptionScheme):
222     """
223     A wrapper for OpenPGP keys management and use (encryption, decyption,
224     signing and verification).
225     """
226
227     # type used on the soledad documents
228     KEY_TYPE = OpenPGPKey.__name__
229     ACTIVE_TYPE = KEY_TYPE + KEYMANAGER_ACTIVE_TYPE
230
231     def __init__(self, soledad, gpgbinary=None):
232         """
233         Initialize the OpenPGP wrapper.
234
235         :param soledad: A Soledad instance for key storage.
236         :type soledad: leap.soledad.Soledad
237         :param gpgbinary: Name for GnuPG binary executable.
238         :type gpgbinary: C{str}
239         """
240         EncryptionScheme.__init__(self, soledad)
241         self._gpgbinary = gpgbinary
242
243     #
244     # Keys management
245     #
246
247     def gen_key(self, address):
248         """
249         Generate an OpenPGP keypair bound to C{address}.
250
251         :param address: The address bound to the key.
252         :type address: str
253         :return: The key bound to C{address}.
254         :rtype: OpenPGPKey
255         @raise KeyAlreadyExists: If key already exists in local database.
256         """
257         # make sure the key does not already exist
258         leap_assert(is_address(address), 'Not an user address: %s' % address)
259         try:
260             self.get_key(address)
261             raise errors.KeyAlreadyExists(address)
262         except errors.KeyNotFound:
263             logger.debug('Key for %s not found' % (address,))
264
265         with self._temporary_gpgwrapper() as gpg:
266             # TODO: inspect result, or use decorator
267             params = gpg.gen_key_input(
268                 key_type='RSA',
269                 key_length=4096,
270                 name_real=address,
271                 name_email=address,
272                 name_comment='')
273             logger.info("About to generate keys... This might take SOME time.")
274             gpg.gen_key(params)
275             logger.info("Keys for %s have been successfully "
276                         "generated." % (address,))
277             pubkeys = gpg.list_keys()
278
279             # assert for new key characteristics
280
281             # XXX This exception is not properly catched by the soledad
282             # bootstrapping, so if we do not finish generating the keys
283             # we end with a blocked thread -- kali
284
285             leap_assert(
286                 len(pubkeys) is 1,  # a unitary keyring!
287                 'Keyring has wrong number of keys: %d.' % len(pubkeys))
288             key = gpg.list_keys(secret=True).pop()
289             leap_assert(
290                 len(key['uids']) is 1,  # with just one uid!
291                 'Wrong number of uids for key: %d.' % len(key['uids']))
292             leap_assert(
293                 re.match('.*<%s>$' % address, key['uids'][0]) is not None,
294                 'Key not correctly bound to address.')
295             # insert both public and private keys in storage
296             for secret in [True, False]:
297                 key = gpg.list_keys(secret=secret).pop()
298                 openpgp_key = _build_key_from_gpg(
299                     address, key,
300                     gpg.export_keys(key['fingerprint'], secret=secret))
301                 self.put_key(openpgp_key, address)
302
303         return self.get_key(address, private=True)
304
305     def get_key(self, address, private=False):
306         """
307         Get key bound to C{address} from local storage.
308
309         :param address: The address bound to the key.
310         :type address: str
311         :param private: Look for a private key instead of a public one?
312         :type private: bool
313
314         :return: The key bound to C{address}.
315         :rtype: OpenPGPKey
316         @raise KeyNotFound: If the key was not found on local storage.
317         """
318         address = _parse_address(address)
319
320         doc = self._get_key_doc(address, private)
321         if doc is None:
322             raise errors.KeyNotFound(address)
323         return build_key_from_dict(OpenPGPKey, address, doc.content)
324
325     def parse_ascii_key(self, key_data):
326         """
327         Parses an ascii armored key (or key pair) data and returns
328         the OpenPGPKey keys.
329
330         :param key_data: the key data to be parsed.
331         :type key_data: str or unicode
332
333         :returns: the public key and private key (if applies) for that data.
334         :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey)
335                 the tuple may have one or both components None
336         """
337         leap_assert_type(key_data, (str, unicode))
338         # TODO: add more checks for correct key data.
339         leap_assert(key_data is not None, 'Data does not represent a key.')
340         mail_regex = '.*<([\w.-]+@[\w.-]+)>.*'
341
342         with self._temporary_gpgwrapper() as gpg:
343             # TODO: inspect result, or use decorator
344             gpg.import_keys(key_data)
345             privkey = None
346             pubkey = None
347
348             try:
349                 privkey = gpg.list_keys(secret=True).pop()
350             except IndexError:
351                 pass
352             try:
353                 pubkey = gpg.list_keys(secret=False).pop()  # unitary keyring
354             except IndexError:
355                 return (None, None)
356
357             # extract adress from first uid on key
358             match = re.match(mail_regex, pubkey['uids'].pop())
359             leap_assert(match is not None, 'No user address in key data.')
360             address = match.group(1)
361
362             openpgp_privkey = None
363             if privkey is not None:
364                 match = re.match(mail_regex, privkey['uids'].pop())
365                 leap_assert(match is not None, 'No user address in key data.')
366                 privaddress = match.group(1)
367
368                 # build private key
369                 openpgp_privkey = _build_key_from_gpg(
370                     privaddress, privkey,
371                     gpg.export_keys(privkey['fingerprint'], secret=True))
372
373                 leap_check(address == privaddress,
374                            'Addresses in public and private key differ.',
375                            errors.KeyAddressMismatch)
376                 leap_check(pubkey['fingerprint'] == privkey['fingerprint'],
377                            'Fingerprints for public and private key differ.',
378                            errors.KeyFingerprintMismatch)
379
380             # build public key
381             openpgp_pubkey = _build_key_from_gpg(
382                 address, pubkey,
383                 gpg.export_keys(pubkey['fingerprint'], secret=False))
384
385             return (openpgp_pubkey, openpgp_privkey)
386
387     def put_ascii_key(self, key_data, address=None):
388         """
389         Put key contained in ascii-armored C{key_data} in local storage.
390
391         :param key_data: The key data to be stored.
392         :type key_data: str or unicode
393         :param address: address for which this key will be active. If not set
394                         all the uids will be activated
395         :type address: str
396         """
397         leap_assert_type(key_data, (str, unicode))
398
399         openpgp_privkey = None
400         try:
401             openpgp_pubkey, openpgp_privkey = self.parse_ascii_key(key_data)
402         except (errors.KeyAddressMismatch, errors.KeyFingerprintMismatch) as e:
403             leap_assert(False, repr(e))
404
405         if openpgp_pubkey is not None:
406             self.put_key(openpgp_pubkey, address)
407         if openpgp_privkey is not None:
408             self.put_key(openpgp_privkey, address)
409
410     def put_key(self, key, address=None):
411         """
412         Put C{key} in local storage.
413
414         :param key: The key to be stored.
415         :type key: OpenPGPKey
416         :param address: address for which this key will be active. If not set
417                         all the uids will be activated
418         :type address: str
419         """
420         if address is not None:
421             active_address = [_parse_address(address)]
422         else:
423             active_address = key.address
424
425         self._put_key_doc(key)
426         self._put_active_doc(key, active_address)
427
428     def _put_key_doc(self, key):
429         """
430         Put key document in soledad
431
432         :type key: OpenPGPKey
433         """
434         docs = self._soledad.get_from_index(
435             TYPE_ID_PRIVATE_INDEX,
436             self.KEY_TYPE,
437             key.key_id,
438             '1' if key.private else '0')
439         if len(docs) != 0:
440             doc = docs.pop()
441             if key.fingerprint == doc.content[KEY_FINGERPRINT_KEY]:
442                 # in case of an update of the key merge them with gnupg
443                 with self._temporary_gpgwrapper() as gpg:
444                     gpg.import_keys(doc.content[KEY_DATA_KEY])
445                     gpg.import_keys(key.key_data)
446                     gpgkey = gpg.list_keys(secret=key.private).pop()
447                     key = _build_key_from_gpg(
448                         key.address[0], gpgkey,
449                         gpg.export_keys(gpgkey['fingerprint'],
450                                         secret=key.private))
451                 doc.set_json(key.get_json())
452                 self._soledad.put_doc(doc)
453             else:
454                 logger.critical(
455                     "Can't put a key whith the same key_id and different "
456                     "fingerprint: %s, %s"
457                     % (key.fingerprint, doc.content[KEY_FINGERPRINT_KEY]))
458         else:
459             self._soledad.create_doc_from_json(key.get_json())
460
461     def _put_active_doc(self, key, addresses):
462         """
463         Put active key document in soledad
464
465         :type key: OpenPGPKey
466         :type addresses: list(str)
467         """
468         for address in addresses:
469             docs = self._soledad.get_from_index(
470                 TYPE_ADDRESS_PRIVATE_INDEX,
471                 self.ACTIVE_TYPE,
472                 address,
473                 '1' if key.private else '0')
474             if len(docs) == 1:
475                 doc = docs.pop()
476                 doc.set_json(key.get_active_json(address))
477                 self._soledad.put_doc(doc)
478             else:
479                 if len(docs) > 1:
480                     logger.error("There is more than one active key document "
481                                  "for the address %s" % (address,))
482                     for doc in docs:
483                         self._soledad.delete_doc(doc)
484                 self._soledad.create_doc_from_json(
485                     key.get_active_json(address))
486
487     def _get_key_doc(self, address, private=False):
488         """
489         Get the document with a key (public, by default) bound to C{address}.
490
491         If C{private} is True, looks for a private key instead of a public.
492
493         :param address: The address bound to the key.
494         :type address: str
495         :param private: Whether to look for a private key.
496         :type private: bool
497         :return: The document with the key or None if it does not exist.
498         :rtype: leap.soledad.document.SoledadDocument
499         """
500         activedoc = self._soledad.get_from_index(
501             TYPE_ADDRESS_PRIVATE_INDEX,
502             self.ACTIVE_TYPE,
503             address,
504             '1' if private else '0')
505         if len(activedoc) is 0:
506             return None
507         leap_assert(
508             len(activedoc) is 1,
509             'Found more than one key for address %s!' % (address,))
510
511         key_id = activedoc[0].content[KEY_ID_KEY]
512         doclist = self._soledad.get_from_index(
513             TYPE_ID_PRIVATE_INDEX,
514             self.KEY_TYPE,
515             key_id,
516             '1' if private else '0')
517         leap_assert(
518             len(doclist) is 1,
519             'There is %d keys for id %s!' % (len(doclist), key_id))
520         return doclist.pop()
521
522     def delete_key(self, key):
523         """
524         Remove C{key} from storage.
525
526         May raise:
527             errors.KeyNotFound
528
529         :param key: The key to be removed.
530         :type key: EncryptionKey
531         """
532         leap_assert_type(key, OpenPGPKey)
533         activedocs = self._soledad.get_from_index(
534             TYPE_ID_PRIVATE_INDEX,
535             self.ACTIVE_TYPE,
536             key.key_id,
537             '1' if key.private else '0')
538         for doc in activedocs:
539             self._soledad.delete_doc(doc)
540
541         docs = self._soledad.get_from_index(
542             TYPE_ID_PRIVATE_INDEX,
543             self.KEY_TYPE,
544             key.key_id,
545             '1' if key.private else '0')
546         if len(docs) == 0:
547             raise errors.KeyNotFound(key)
548         if len(docs) > 1:
549             logger.critical("There is more than one key for key_id %s"
550                             % key.key_id)
551
552         doc = None
553         for d in docs:
554             if d.content['fingerprint'] == key.fingerprint:
555                 doc = d
556                 break
557         if doc is None:
558             raise errors.KeyNotFound(key)
559         self._soledad.delete_doc(doc)
560
561     #
562     # Data encryption, decryption, signing and verifying
563     #
564
565     def _temporary_gpgwrapper(self, keys=None):
566         """
567         Return a gpg wrapper that implements the context manager protocol and
568         contains C{keys}.
569
570         :param keys: keys to conform the keyring.
571         :type key: list(OpenPGPKey)
572
573         :return: a TempGPGWrapper instance
574         :rtype: TempGPGWrapper
575         """
576         # TODO do here checks on key_data
577         return TempGPGWrapper(
578             keys=keys, gpgbinary=self._gpgbinary)
579
580     @staticmethod
581     def _assert_gpg_result_ok(result):
582         """
583         Check if GPG result is 'ok' and log stderr outputs.
584
585         :param result: GPG results, which have a field calld 'ok' that states
586                        whether the gpg operation was successful or not.
587         :type result: object
588
589         :raise GPGError: Raised when the gpg operation was not successful.
590         """
591         stderr = getattr(result, 'stderr', None)
592         if stderr:
593             logger.debug("%s" % (stderr,))
594         if getattr(result, 'ok', None) is not True:
595             raise errors.GPGError(
596                 'Failed to encrypt/decrypt: %s' % stderr)
597
598     def encrypt(self, data, pubkey, passphrase=None, sign=None,
599                 cipher_algo='AES256'):
600         """
601         Encrypt C{data} using public @{pubkey} and sign with C{sign} key.
602
603         :param data: The data to be encrypted.
604         :type data: str
605         :param pubkey: The key used to encrypt.
606         :type pubkey: OpenPGPKey
607         :param sign: The key used for signing.
608         :type sign: OpenPGPKey
609         :param cipher_algo: The cipher algorithm to use.
610         :type cipher_algo: str
611
612         :return: The encrypted data.
613         :rtype: str
614
615         :raise EncryptError: Raised if failed encrypting for some reason.
616         """
617         leap_assert_type(pubkey, OpenPGPKey)
618         leap_assert(pubkey.private is False, 'Key is not public.')
619         keys = [pubkey]
620         if sign is not None:
621             leap_assert_type(sign, OpenPGPKey)
622             leap_assert(sign.private is True)
623             keys.append(sign)
624         with self._temporary_gpgwrapper(keys) as gpg:
625             result = gpg.encrypt(
626                 data, pubkey.fingerprint,
627                 default_key=sign.key_id if sign else None,
628                 passphrase=passphrase, symmetric=False,
629                 cipher_algo=cipher_algo)
630             # Here we cannot assert for correctness of sig because the sig is
631             # in the ciphertext.
632             # result.ok    - (bool) indicates if the operation succeeded
633             # result.data  - (bool) contains the result of the operation
634             try:
635                 self._assert_gpg_result_ok(result)
636                 return result.data
637             except errors.GPGError as e:
638                 logger.error('Failed to decrypt: %s.' % str(e))
639                 raise errors.EncryptError()
640
641     def decrypt(self, data, privkey, passphrase=None, verify=None):
642         """
643         Decrypt C{data} using private @{privkey} and verify with C{verify} key.
644
645         :param data: The data to be decrypted.
646         :type data: str
647         :param privkey: The key used to decrypt.
648         :type privkey: OpenPGPKey
649         :param passphrase: The passphrase for the secret key used for
650                            decryption.
651         :type passphrase: str
652         :param verify: The key used to verify a signature.
653         :type verify: OpenPGPKey
654
655         :return: The decrypted data.
656         :rtype: unicode
657
658         :raise DecryptError: Raised if failed decrypting for some reason.
659         :raise InvalidSignature: Raised if unable to verify the signature with
660                                  C{verify} key.
661         """
662         leap_assert(privkey.private is True, 'Key is not private.')
663         keys = [privkey]
664         if verify is not None:
665             leap_assert_type(verify, OpenPGPKey)
666             leap_assert(verify.private is False)
667             keys.append(verify)
668         with self._temporary_gpgwrapper(keys) as gpg:
669             try:
670                 result = gpg.decrypt(
671                     data, passphrase=passphrase, always_trust=True)
672                 self._assert_gpg_result_ok(result)
673                 # verify signature
674                 if (verify is not None):
675                     if result.valid is False or \
676                             verify.fingerprint != result.pubkey_fingerprint:
677                         raise errors.InvalidSignature(
678                             'Failed to verify signature with key %s: %s' %
679                             (verify.key_id, result.stderr))
680
681                 return result.data
682             except errors.GPGError as e:
683                 logger.error('Failed to decrypt: %s.' % str(e))
684                 raise errors.DecryptError(str(e))
685
686     def is_encrypted(self, data):
687         """
688         Return whether C{data} was asymmetrically encrypted using OpenPGP.
689
690         :param data: The data we want to know about.
691         :type data: str
692
693         :return: Whether C{data} was encrypted using this wrapper.
694         :rtype: bool
695         """
696         with self._temporary_gpgwrapper() as gpg:
697             gpgutil = GPGUtilities(gpg)
698             return gpgutil.is_encrypted_asym(data)
699
700     def sign(self, data, privkey, digest_algo='SHA512', clearsign=False,
701              detach=True, binary=False):
702         """
703         Sign C{data} with C{privkey}.
704
705         :param data: The data to be signed.
706         :type data: str
707
708         :param privkey: The private key to be used to sign.
709         :type privkey: OpenPGPKey
710         :param digest_algo: The hash digest to use.
711         :type digest_algo: str
712         :param clearsign: If True, create a cleartext signature.
713         :type clearsign: bool
714         :param detach: If True, create a detached signature.
715         :type detach: bool
716         :param binary: If True, do not ascii armour the output.
717         :type binary: bool
718
719         :return: The ascii-armored signed data.
720         :rtype: str
721         """
722         leap_assert_type(privkey, OpenPGPKey)
723         leap_assert(privkey.private is True)
724
725         # result.fingerprint - contains the fingerprint of the key used to
726         #                      sign.
727         with self._temporary_gpgwrapper(privkey) as gpg:
728             result = gpg.sign(data, default_key=privkey.key_id,
729                               digest_algo=digest_algo, clearsign=clearsign,
730                               detach=detach, binary=binary)
731             rfprint = privkey.fingerprint
732             privkey = gpg.list_keys(secret=True).pop()
733             kfprint = privkey['fingerprint']
734             if result.fingerprint is None:
735                 raise errors.SignFailed(
736                     'Failed to sign with key %s: %s' %
737                     (privkey['keyid'], result.stderr))
738             leap_assert(
739                 result.fingerprint == kfprint,
740                 'Signature and private key fingerprints mismatch: '
741                 '%s != %s' % (rfprint, kfprint))
742         return result.data
743
744     def verify(self, data, pubkey, detached_sig=None):
745         """
746         Verify signed C{data} with C{pubkey}, eventually using
747         C{detached_sig}.
748
749         :param data: The data to be verified.
750         :type data: str
751         :param pubkey: The public key to be used on verification.
752         :type pubkey: OpenPGPKey
753         :param detached_sig: A detached signature. If given, C{data} is
754                              verified against this detached signature.
755         :type detached_sig: str
756
757         :return: The ascii-armored signed data.
758         :rtype: str
759         """
760         leap_assert_type(pubkey, OpenPGPKey)
761         leap_assert(pubkey.private is False)
762         with self._temporary_gpgwrapper(pubkey) as gpg:
763             result = None
764             if detached_sig is None:
765                 result = gpg.verify(data)
766             else:
767                 # to verify using a detached sig we have to use
768                 # gpg.verify_file(), which receives the data as a binary
769                 # stream and the name of a file containing the signature.
770                 sf, sfname = tempfile.mkstemp()
771                 with os.fdopen(sf, 'w') as sfd:
772                     sfd.write(detached_sig)
773                 result = gpg.verify_file(io.BytesIO(data), sig_file=sfname)
774                 os.unlink(sfname)
775             gpgpubkey = gpg.list_keys().pop()
776             valid = result.valid
777             rfprint = result.fingerprint
778             kfprint = gpgpubkey['fingerprint']
779             # raise in case sig is invalid
780             if valid is False or rfprint != kfprint:
781                 raise errors.InvalidSignature(
782                     'Failed to verify signature '
783                     'with key %s.' % gpgpubkey['keyid'])
784             return True