diff options
author | drebs <drebs@leap.se> | 2013-04-15 10:41:56 -0300 |
---|---|---|
committer | drebs <drebs@leap.se> | 2013-04-15 14:37:01 -0300 |
commit | 314bc876d564cd6265cc8eb4095e423f1140349a (patch) | |
tree | d1f3f8f332e76c9db17d7178b1f88884a5cf568a /src/leap/common/keymanager | |
parent | 1858a50809dc48bfcc4ad2d96dd5641f1de6b9eb (diff) |
Add basic openpgp key handling to Key Manager
Diffstat (limited to 'src/leap/common/keymanager')
-rw-r--r-- | src/leap/common/keymanager/__init__.py | 116 | ||||
-rw-r--r-- | src/leap/common/keymanager/errors.py | 29 | ||||
-rw-r--r-- | src/leap/common/keymanager/gpg.py | 398 | ||||
-rw-r--r-- | src/leap/common/keymanager/keys.py | 127 | ||||
-rw-r--r-- | src/leap/common/keymanager/openpgp.py | 126 |
5 files changed, 692 insertions, 104 deletions
diff --git a/src/leap/common/keymanager/__init__.py b/src/leap/common/keymanager/__init__.py index 71aaddd..10acb36 100644 --- a/src/leap/common/keymanager/__init__.py +++ b/src/leap/common/keymanager/__init__.py @@ -27,114 +27,22 @@ except ImportError: import json # noqa -from abc import ABCMeta, abstractmethod from u1db.errors import HTTPError -# -# Key types -# - -class EncryptionKey(object): - """ - Abstract class for encryption keys. - - A key is "validated" if the nicknym agent has bound the user address to a - public key. Nicknym supports three different levels of key validation: - - * Level 3 - path trusted: A path of cryptographic signatures can be traced - from a trusted key to the key under evaluation. By default, only the - provider key from the user's provider is a "trusted key". - * level 2 - provider signed: The key has been signed by a provider key for - the same domain, but the provider key is not validated using a trust - path (i.e. it is only registered) - * level 1 - registered: The key has been encountered and saved, it has no - signatures (that are meaningful to the nicknym agent). - """ - - __metaclass__ = ABCMeta - - def __init__(self, address, key_id=None, fingerprint=None, - key_data=None, length=None, expiry_date=None, - validation=None, first_seen_at=None, - last_audited_at=None): - self.address = address - self.key_id = key_id - self.fingerprint = fingerprint - self.key_data = key_data - self.length = length - self.expiry_date = expiry_date - self.validation = validation - self.first_seen_at = first_seen_at - self.last_audited_at = last_audited_at - - @abstractmethod - def get_json(self): - """ - Return a JSON string describing this key. - - @return: The JSON string describing this key. - @rtype: str - """ - - -# -# Key wrappers -# - -class KeyTypeWrapper(object): - """ - Abstract class for Key Type Wrappers. - - A wrapper for a certain key type should know how to get and put keys in - local storage using Soledad and also how to generate new keys. - """ - - __metaclass__ = ABCMeta - - @abstractmethod - def get_key(self, address): - """ - Get key from local storage. - - @param address: The address bound to the key. - @type address: str - - @return: The key bound to C{address}. - @rtype: EncryptionKey - @raise KeyNotFound: If the key was not found on local storage. - """ - - @abstractmethod - def put_key(self, key): - """ - Put a key in local storage. - - @param key: The key to be stored. - @type key: EncryptionKey - """ - - @abstractmethod - def gen_key(self, address): - """ - Generate a new key. - - @param address: The address bound to the key. - @type address: str - @return: The key bound to C{address}. - @rtype: EncryptionKey - """ - - -# -# Key manager -# +from leap.common.keymanager.errors import ( + KeyNotFound, + KeyAlreadyExists, +) +from leap.common.keymanager.openpgp import ( + OpenPGPKey, + OpenPGPWrapper, +) -class KeyNotFound(Exception): - """ - Raised when key was no found on keyserver. - """ +wrapper_map = { + OpenPGPKey: OpenPGPWrapper(), +} class KeyManager(object): @@ -195,7 +103,7 @@ class KeyManager(object): except KeyNotFound: key = filter(lambda k: isinstance(k, ktype), self._fetch_keys(address)) - if key is None + if key is None: raise KeyNotFound() wrapper_map[ktype].put_key(key) return key diff --git a/src/leap/common/keymanager/errors.py b/src/leap/common/keymanager/errors.py new file mode 100644 index 0000000..f5bb1ab --- /dev/null +++ b/src/leap/common/keymanager/errors.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# errors.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + + +class KeyNotFound(Exception): + """ + Raised when key was no found on keyserver. + """ + + +class KeyAlreadyExists(Exception): + """ + Raised when attempted to create a key that already exists. + """ diff --git a/src/leap/common/keymanager/gpg.py b/src/leap/common/keymanager/gpg.py new file mode 100644 index 0000000..dc5d791 --- /dev/null +++ b/src/leap/common/keymanager/gpg.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- +# gpgwrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +""" +A GPG wrapper used to handle OpenPGP keys. + +This is a temporary class that will be superseded by the a revised version of +python-gnupg. +""" + + +import os +import gnupg +import re +from gnupg import ( + logger, + _is_sequence, + _make_binary_stream, +) + + +class ListPackets(): + """ + Handle status messages for --list-packets. + """ + + def __init__(self, gpg): + """ + Initialize the packet listing handling class. + + @param gpg: GPG object instance. + @type gpg: gnupg.GPG + """ + self.gpg = gpg + self.nodata = None + self.key = None + self.need_passphrase = None + self.need_passphrase_sym = None + self.userid_hint = None + + def handle_status(self, key, value): + """ + Handle one line of the --list-packets status message. + + @param key: The status message key. + @type key: str + @param value: The status message value. + @type value: str + """ + # TODO: write tests for handle_status + if key == 'NODATA': + self.nodata = True + if key == 'ENC_TO': + # This will only capture keys in our keyring. In the future we + # may want to include multiple unknown keys in this list. + self.key, _, _ = value.split() + if key == 'NEED_PASSPHRASE': + self.need_passphrase = True + if key == 'NEED_PASSPHRASE_SYM': + self.need_passphrase_sym = True + if key == 'USERID_HINT': + self.userid_hint = value.strip().split() + + +class GPGWrapper(gnupg.GPG): + """ + This is a temporary class for handling GPG requests, and should be + replaced by a more general class used throughout the project. + """ + + GNUPG_HOME = os.environ['HOME'] + "/.config/leap/gnupg" + GNUPG_BINARY = "/usr/bin/gpg" # this has to be changed based on OS + + def __init__(self, gpgbinary=GNUPG_BINARY, gnupghome=GNUPG_HOME, + verbose=False, use_agent=False, keyring=None, options=None): + """ + Initialize a GnuPG process wrapper. + + @param gpgbinary: Name for GnuPG binary executable. + @type gpgbinary: C{str} + @param gpghome: Full pathname to directory containing the public and + private keyrings. + @type gpghome: C{str} + @param keyring: Name of alternative keyring file to use. If specified, + the default keyring is not used. + @param verbose: Should some verbose info be output? + @type verbose: bool + @param use_agent: Should pass `--use-agent` to GPG binary? + @type use_agent: bool + @param keyring: Path for the keyring to use. + @type keyring: str + @options: A list of additional options to pass to the GPG binary. + @type options: list + + @raise: RuntimeError with explanation message if there is a problem + invoking gpg. + """ + gnupg.GPG.__init__(self, gnupghome=gnupghome, gpgbinary=gpgbinary, + verbose=verbose, use_agent=use_agent, + keyring=keyring, options=options) + self.result_map['list-packets'] = ListPackets + + def find_key_by_email(self, email, secret=False): + """ + Find user's key based on their email. + + @param email: Email address of key being searched for. + @type email: str + @param secret: Should we search for a secret key? + @type secret: bool + + @return: The fingerprint of the found key. + @rtype: str + """ + for key in self.list_keys(secret=secret): + for uid in key['uids']: + if re.search(email, uid): + return key + raise LookupError("GnuPG public key for email %s not found!" % email) + + def find_key_by_subkey(self, subkey, secret=False): + """ + Find user's key based on a subkey fingerprint. + + @param email: Subkey fingerprint of the key being searched for. + @type email: str + @param secret: Should we search for a secret key? + @type secret: bool + + @return: The fingerprint of the found key. + @rtype: str + """ + for key in self.list_keys(secret=secret): + for sub in key['subkeys']: + if sub[0] == subkey: + return key + raise LookupError( + "GnuPG public key for subkey %s not found!" % subkey) + + def find_key_by_keyid(self, keyid, secret=False): + """ + Find user's key based on the key ID. + + @param email: The key ID of the key being searched for. + @type email: str + @param secret: Should we search for a secret key? + @type secret: bool + + @return: The fingerprint of the found key. + @rtype: str + """ + for key in self.list_keys(secret=secret): + if keyid == key['keyid']: + return key + raise LookupError( + "GnuPG public key for keyid %s not found!" % keyid) + + def find_key_by_fingerprint(self, fingerprint, secret=False): + """ + Find user's key based on the key fingerprint. + + @param email: The fingerprint of the key being searched for. + @type email: str + @param secret: Should we search for a secret key? + @type secret: bool + + @return: The fingerprint of the found key. + @rtype: str + """ + for key in self.list_keys(secret=secret): + if fingerprint == key['fingerprint']: + return key + raise LookupError( + "GnuPG public key for fingerprint %s not found!" % fingerprint) + + def encrypt(self, data, recipient, sign=None, always_trust=True, + passphrase=None, symmetric=False): + """ + Encrypt data using GPG. + + @param data: The data to be encrypted. + @type data: str + @param recipient: The address of the public key to be used. + @type recipient: str + @param sign: Should the encrypted content be signed? + @type sign: bool + @param always_trust: Skip key validation and assume that used keys + are always fully trusted? + @type always_trust: bool + @param passphrase: The passphrase to be used if symmetric encryption + is desired. + @type passphrase: str + @param symmetric: Should we encrypt to a password? + @type symmetric: bool + + @return: An object with encrypted result in the `data` field. + @rtype: gnupg.Crypt + """ + # TODO: devise a way so we don't need to "always trust". + return gnupg.GPG.encrypt(self, data, recipient, sign=sign, + always_trust=always_trust, + passphrase=passphrase, + symmetric=symmetric, + cipher_algo='AES256') + + def decrypt(self, data, always_trust=True, passphrase=None): + """ + Decrypt data using GPG. + + @param data: The data to be decrypted. + @type data: str + @param always_trust: Skip key validation and assume that used keys + are always fully trusted? + @type always_trust: bool + @param passphrase: The passphrase to be used if symmetric encryption + is desired. + @type passphrase: str + + @return: An object with decrypted result in the `data` field. + @rtype: gnupg.Crypt + """ + # TODO: devise a way so we don't need to "always trust". + return gnupg.GPG.decrypt(self, data, always_trust=always_trust, + passphrase=passphrase) + + def send_keys(self, keyserver, *keyids): + """ + Send keys to a keyserver + + @param keyserver: The keyserver to send the keys to. + @type keyserver: str + @param keyids: The key ids to send. + @type keyids: list + + @return: A list of keys sent to server. + @rtype: gnupg.ListKeys + """ + # TODO: write tests for this. + # TODO: write a SendKeys class to handle status for this. + result = self.result_map['list'](self) + gnupg.logger.debug('send_keys: %r', keyids) + data = gnupg._make_binary_stream("", self.encoding) + args = ['--keyserver', keyserver, '--send-keys'] + args.extend(keyids) + self._handle_io(args, data, result, binary=True) + gnupg.logger.debug('send_keys result: %r', result.__dict__) + data.close() + return result + + def encrypt_file(self, file, recipients, sign=None, + always_trust=False, passphrase=None, + armor=True, output=None, symmetric=False, + cipher_algo=None): + """ + Encrypt the message read from the file-like object 'file'. + + @param file: The file to be encrypted. + @type data: file + @param recipient: The address of the public key to be used. + @type recipient: str + @param sign: Should the encrypted content be signed? + @type sign: bool + @param always_trust: Skip key validation and assume that used keys + are always fully trusted? + @type always_trust: bool + @param passphrase: The passphrase to be used if symmetric encryption + is desired. + @type passphrase: str + @param armor: Create ASCII armored output? + @type armor: bool + @param output: Path of file to write results in. + @type output: str + @param symmetric: Should we encrypt to a password? + @type symmetric: bool + @param cipher_algo: Algorithm to use. + @type cipher_algo: str + + @return: An object with encrypted result in the `data` field. + @rtype: gnupg.Crypt + """ + args = ['--encrypt'] + if symmetric: + args = ['--symmetric'] + if cipher_algo: + args.append('--cipher-algo %s' % cipher_algo) + else: + args = ['--encrypt'] + if not _is_sequence(recipients): + recipients = (recipients,) + for recipient in recipients: + args.append('--recipient "%s"' % recipient) + if armor: # create ascii-armored output - set to False for binary + args.append('--armor') + if output: # write the output to a file with the specified name + if os.path.exists(output): + os.remove(output) # to avoid overwrite confirmation message + args.append('--output "%s"' % output) + if sign: + args.append('--sign --default-key "%s"' % sign) + if always_trust: + args.append("--always-trust") + result = self.result_map['crypt'](self) + self._handle_io(args, file, result, passphrase=passphrase, binary=True) + logger.debug('encrypt result: %r', result.data) + return result + + def list_packets(self, data): + """ + List the sequence of packets. + + @param data: The data to extract packets from. + @type data: str + + @return: An object with packet info. + @rtype ListPackets + """ + args = ["--list-packets"] + result = self.result_map['list-packets'](self) + self._handle_io( + args, + _make_binary_stream(data, self.encoding), + result, + ) + return result + + def encrypted_to(self, data): + """ + Return the key to which data is encrypted to. + + @param data: The data to be examined. + @type data: str + + @return: The fingerprint of the key to which data is encrypted to. + @rtype: str + """ + # TODO: make this support multiple keys. + result = self.list_packets(data) + if not result.key: + raise LookupError( + "Content is not encrypted to a GnuPG key!") + try: + return self.find_key_by_keyid(result.key) + except: + return self.find_key_by_subkey(result.key) + + def is_encrypted_sym(self, data): + """ + Say whether some chunk of data is encrypted to a symmetric key. + + @param data: The data to be examined. + @type data: str + + @return: Whether data is encrypted to a symmetric key. + @rtype: bool + """ + result = self.list_packets(data) + return bool(result.need_passphrase_sym) + + def is_encrypted_asym(self, data): + """ + Say whether some chunk of data is encrypted to a private key. + + @param data: The data to be examined. + @type data: str + + @return: Whether data is encrypted to a private key. + @rtype: bool + """ + result = self.list_packets(data) + return bool(result.key) + + def is_encrypted(self, data): + """ + Say whether some chunk of data is encrypted to a key. + + @param data: The data to be examined. + @type data: str + + @return: Whether data is encrypted to a key. + @rtype: bool + """ + self.is_encrypted_asym() or self.is_encrypted_sym() + diff --git a/src/leap/common/keymanager/keys.py b/src/leap/common/keymanager/keys.py new file mode 100644 index 0000000..13e3c0b --- /dev/null +++ b/src/leap/common/keymanager/keys.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# keys.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +""" +Abstact key type and wrapper representations. +""" + + +from abc import ABCMeta, abstractmethod + + +class EncryptionKey(object): + """ + Abstract class for encryption keys. + + A key is "validated" if the nicknym agent has bound the user address to a + public key. Nicknym supports three different levels of key validation: + + * Level 3 - path trusted: A path of cryptographic signatures can be traced + from a trusted key to the key under evaluation. By default, only the + provider key from the user's provider is a "trusted key". + * level 2 - provider signed: The key has been signed by a provider key for + the same domain, but the provider key is not validated using a trust + path (i.e. it is only registered) + * level 1 - registered: The key has been encountered and saved, it has no + signatures (that are meaningful to the nicknym agent). + """ + + __metaclass__ = ABCMeta + + def __init__(self, address, key_id=None, fingerprint=None, + key_data=None, length=None, expiry_date=None, + validation=None, first_seen_at=None, + last_audited_at=None): + self.address = address + self.key_id = key_id + self.fingerprint = fingerprint + self.key_data = key_data + self.length = length + self.expiry_date = expiry_date + self.validation = validation + self.first_seen_at = first_seen_at + self.last_audited_at = last_audited_at + + def get_json(self): + """ + Return a JSON string describing this key. + + @return: The JSON string describing this key. + @rtype: str + """ + return json.dumps({ + 'address': self.address, + 'type': str(self.__type__), + 'key_id': self.key_id, + 'fingerprint': self.fingerprint, + 'key_data': self.key_data, + 'length': self.length, + 'expiry_date': self.expiry_date, + 'validation': self.validation, + 'first_seen_at': self.first_seen_at, + 'last_audited_at': self.last_audited_at, + }) + + +# +# Key wrappers +# + +class KeyTypeWrapper(object): + """ + Abstract class for Key Type Wrappers. + + A wrapper for a certain key type should know how to get and put keys in + local storage using Soledad and also how to generate new keys. + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def get_key(self, address): + """ + Get key from local storage. + + @param address: The address bound to the key. + @type address: str + + @return: The key bound to C{address}. + @rtype: EncryptionKey + @raise KeyNotFound: If the key was not found on local storage. + """ + + @abstractmethod + def put_key(self, key): + """ + Put a key in local storage. + + @param key: The key to be stored. + @type key: EncryptionKey + """ + + @abstractmethod + def gen_key(self, address): + """ + Generate a new key. + + @param address: The address bound to the key. + @type address: str + @return: The key bound to C{address}. + @rtype: EncryptionKey + """ + diff --git a/src/leap/common/keymanager/openpgp.py b/src/leap/common/keymanager/openpgp.py new file mode 100644 index 0000000..bb73089 --- /dev/null +++ b/src/leap/common/keymanager/openpgp.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# openpgpwrapper.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +""" +Infrastructure for using OpenPGP keys in Key Manager. +""" + + +import re + +from leap.common.keymanager.errors import ( + KeyNotFound, + KeyAlreadyExists, +) +from leap.common.keymanager.keys import ( + EncryptionKey, + KeyTypeWrapper, +) +from leap.common.keymanager.gpg import GPGWrapper + + +class OpenPGPKey(EncryptionKey): + """ + Base class for OpenPGP keys. + """ + + +class OpenPGPWrapper(KeyTypeWrapper): + """ + A wrapper for OpenPGP keys. + """ + + def __init__(self, gnupghome=None): + self._gpg = GPGWrapper(gnupghome=gnupghome) + + def _build_key(self, address, result): + """ + Build an OpenPGPWrapper key for C{address} based on C{result} from + local storage. + + @param address: The address bound to the key. + @type address: str + @param result: Result obtained from GPG storage. + @type result: dict + """ + key_data = self._gpg.export_keys(result['fingerprint'], secret=False) + return OpenPGPKey( + address, + key_id=result['keyid'], + fingerprint=result['fingerprint'], + key_data=key_data, + length=result['length'], + expiry_date=result['expires'], + validation=None, # TODO: verify for validation. + ) + + def gen_key(self, address): + """ + Generate an OpenPGP keypair for C{address}. + + @param address: The address bound to the key. + @type address: str + @return: The key bound to C{address}. + @rtype: OpenPGPKey + @raise KeyAlreadyExists: If key already exists in local database. + """ + try: + self.get_key(address) + raise KeyAlreadyExists() + except KeyNotFound: + pass + params = self._gpg.gen_key_input( + key_type='RSA', + key_length=4096, + name_real=address, + name_email=address, + name_comment='Generated by LEAP Key Manager.') + self._gpg.gen_key(params) + return self.get_key(address) + + def get_key(self, address): + """ + Get key bound to C{address} from local storage. + + @param address: The address bound to the key. + @type address: str + + @return: The key bound to C{address}. + @rtype: OpenPGPKey + @raise KeyNotFound: If the key was not found on local storage. + """ + m = re.compile('.*<%s>$' % address) + keys = self._gpg.list_keys(secret=False) + + def bound_to_address(key): + return bool(filter(lambda u: m.match(u), key['uids'])) + + try: + bound_key = filter(bound_to_address, keys).pop() + return self._build_key(address, bound_key) + except IndexError: + raise KeyNotFound(address) + + def put_key(self, data): + """ + Put key contained in {data} in local storage. + + @param key: The key data to be stored. + @type key: str + """ + self._gpg.import_keys(data) |