diff options
| author | J08nY | 2017-06-19 18:55:44 +0200 |
|---|---|---|
| committer | J08nY | 2017-06-19 18:55:44 +0200 |
| commit | c8a7543b99ffa1f28def431e9d77fd74c3f2f00f (patch) | |
| tree | 35a4c8c114a8842c9171ce0acbd3370aceca75f0 | |
| parent | 64ef87abf4599f096743081c7be721bacc255606 (diff) | |
| download | mailman-pgp-c8a7543b99ffa1f28def431e9d77fd74c3f2f00f.tar.gz mailman-pgp-c8a7543b99ffa1f28def431e9d77fd74c3f2f00f.tar.zst mailman-pgp-c8a7543b99ffa1f28def431e9d77fd74c3f2f00f.zip | |
| -rw-r--r-- | setup.py | 4 | ||||
| -rw-r--r-- | src/mailman_pgp/config/mailman_pgp.cfg | 27 | ||||
| -rw-r--r-- | src/mailman_pgp/model/list.py | 67 | ||||
| -rw-r--r-- | src/mailman_pgp/pgp/__init__.py | 71 | ||||
| -rw-r--r-- | src/mailman_pgp/pgp/inline.py | 1 | ||||
| -rw-r--r-- | src/mailman_pgp/pgp/keygen.py | 119 | ||||
| -rw-r--r-- | src/mailman_pgp/pgp/mime.py | 1 | ||||
| -rw-r--r-- | src/mailman_pgp/plugin.py | 7 |
8 files changed, 210 insertions, 87 deletions
@@ -23,9 +23,9 @@ A plugin for GNU Mailman that adds encrypted mailing lists via PGP/MIME.""", package_dir={'': 'src'}, install_requires=[ 'mailman>=3.1.0', - 'gnupg', - 'gpgmime', + 'PGPy', 'atpublic', + 'flufl.lock', 'sqlalchemy', 'zope.interface', 'zope.event' diff --git a/src/mailman_pgp/config/mailman_pgp.cfg b/src/mailman_pgp/config/mailman_pgp.cfg index de87ca1..a287b18 100644 --- a/src/mailman_pgp/config/mailman_pgp.cfg +++ b/src/mailman_pgp/config/mailman_pgp.cfg @@ -5,31 +5,30 @@ url = sqlite:////$DATA_DIR/pgp.db -[gpg] -# GPG homedir. -homedir = $DATA_DIR/gpg/ +[keydirs] +# Key directory used to store user public keys. +user_keydir= $DATA_DIR/gpg/user_keydir/ -# Keyring used to store user keys. (GPG pubring) -keyring = $DATA_DIR/gpg/pubring.gpg +# Key directory used to store list keypairs. +list_keydir = $DATA_DIR/gpg/list_keydir/ -# Keyring used to store list keypairs. (GPG secring) -secring = $DATA_DIR/gpg/secring.gpg - -# The GPG binary to use. -binary = gpg +# Key directory used to store list archive public keys. +archive_keydir = $DATA_DIR/gpg/archive_keydir/ [keypairs] -# +# Length of primary list key. key_length = 4096 -# +# Type of primary list key. +# One of RSA, DSA, ECDSA. key_type = RSA -# +# Length of list encryption subkey. subkey_length = 4096 -# +# Type of list encryption subkey. +# One of RSA, ECDH. subkey_type = RSA diff --git a/src/mailman_pgp/model/list.py b/src/mailman_pgp/model/list.py index 001b6b3..fa5b89b 100644 --- a/src/mailman_pgp/model/list.py +++ b/src/mailman_pgp/model/list.py @@ -1,14 +1,18 @@ """""" +from multiprocessing import SimpleQueue +from os.path import exists, isfile, join + from mailman.config import config as mailman_config, config from mailman.database.types import Enum, SAUnicode from mailman.interfaces.action import Action from mailman.model.mailinglist import MailingList +from pgpy import PGPKey from public import public from sqlalchemy import Boolean, Column, Integer from mailman_pgp.model.base import Base -from mailman_pgp.pgp.keygen import KeyGenerator +from mailman_pgp.pgp.keygen import ListKeyGenerator @public @@ -17,7 +21,6 @@ class EncryptedMailingList(Base): id = Column(Integer, primary_key=True) list_id = Column(SAUnicode, index=True) - _key_fingerprint = Column('key_fingerprint', SAUnicode) unsigned_msg_action = Column(Enum(Action)) nonencrypted_msg_action = Column(Enum(Action)) strip_original_signature = Column(Boolean) @@ -27,28 +30,21 @@ class EncryptedMailingList(Base): super().__init__() self.list_id = mlist.list_id self._mlist = mlist - self._pubkey = None + self._key = None + self._key_queue = None + self._key_generator = None + self._generate(mlist) - self._key_generator = self._create_generator(mlist) + def _generate(self, mlist): + self._key_queue = SimpleQueue() + self._key_generator = ListKeyGenerator(config.gpg.keypair_config, + mlist.display_name, + mlist.posting_address, + mlist.request_address, + self._key_queue, + self.key_path) self._key_generator.start() - def _create_generator(self, mlist): - return KeyGenerator(mlist.list_id, mlist.fqdn_listname) - - @property - def key_fingerprint(self): - if self._key_fingerprint is None: - if self._key_generator.has_key: - self._key_fingerprint = self._key_generator.key_fingerprint - else: - if not self._key_generator.is_alive(): - # TODO this is not the best solution, we should lookup the - # key by mlist.fqdn_listname, if it actually got created - # and key generator didn't receive it. - self._key_generator = self._create_generator(self.mlist) - self._key_generator.start() - return self._key_fingerprint - @property def mlist(self): if self._mlist is not None: @@ -57,9 +53,26 @@ class EncryptedMailingList(Base): _list_id=self.list_id).first() @property - def pubkey(self): - if self._pubkey is None: - if self._key_fingerprint is None: - return None - self._pubkey = config.gpg.export_keys(self._key_fingerprint) - return self._pubkey + def key(self): + if self._key is None: + # First try the queue + if self._key_queue is not None and not self._key_queue.empty(): + self._key = self._key_queue.get() + # Then check the file + elif exists(self.key_path) and isfile(self.key_path): + self._key = PGPKey.from_file(self.key_path) + else: + # Check if key generator is running or what? Restart it if not. + # If we race it shutting down and saving the key file + queue + # it will simply check the key_file exists and put it into a + # queue for us. + if self._key_generator is None or \ + not self._key_generator.is_alive(): + self._generate(self.mlist) + return self._key + + @property + def key_path(self): + return join(config.gpg.keydir_config['list_keydir'], + self.list_id, + '.asc') diff --git a/src/mailman_pgp/pgp/__init__.py b/src/mailman_pgp/pgp/__init__.py index 52506a6..b9aad50 100644 --- a/src/mailman_pgp/pgp/__init__.py +++ b/src/mailman_pgp/pgp/__init__.py @@ -1,39 +1,78 @@ """""" -from pathlib import Path +from os import listdir, makedirs +from os.path import isfile -import gpgmime from mailman.config import config as mailman_config from mailman.utilities.string import expand +from pgpy import PGPKeyring +from pgpy.constants import PubKeyAlgorithm from public import public from mailman_pgp.config import config -GPG_CONFIG_PATHS = ['homedir', 'keyring', 'secring', 'binary'] +KEYDIR_CONFIG_PATHS = ['list_keydir', 'user_keydir', 'archive_keydir'] KEYPAIR_CONFIG_VARIABLES = ['key_type', 'key_length', 'subkey_type', 'subkey_length'] -KEYPAIR_CONFIG_DEFAULTS = { - 'key_usage': 'auth,sign,cert', - 'subkey_usage': 'enc' +KEYPAIR_KEY_TYPE_VALID = ['RSA', 'DSA', 'ECDSA'] +KEYPAIR_SUBKEY_TYPE_VALID = ['RSA', 'ECDH'] +KEYPAIR_TYPE_MAP = { + 'RSA': PubKeyAlgorithm.RSAEncryptOrSign, + 'DSA': PubKeyAlgorithm.DSA, + 'ECDSA': PubKeyAlgorithm.ECDSA, + 'ECDH': PubKeyAlgorithm.ECDH } @public -class GPG(gpgmime.GPG): +class PGP: def __init__(self): + self._load_config() + self._validate_config() + + def _load_config(self): # Get all the [keypairs] config variables. self.keypair_config = dict( (k, config.get('keypairs', k)) for k in KEYPAIR_CONFIG_VARIABLES) - self.keypair_config.update(KEYPAIR_CONFIG_DEFAULTS) # Get and expand all [gpg] config paths against Mailman's directories. - self.gpg_config = dict( - (k, expand(config.get('gpg', k), None, mailman_config.paths)) - for k in GPG_CONFIG_PATHS) + self.keydir_config = dict( + (k, expand(config.get('keydirs', k), None, mailman_config.paths)) + for k in KEYDIR_CONFIG_PATHS) + + def _validate_config(self): + # Validate keypair config + key_type = self.keypair_config['key_type'].upper() + if key_type not in KEYPAIR_KEY_TYPE_VALID: + raise ValueError('Invalid key_type. {}'.format(key_type)) + self.keypair_config['key_type'] = KEYPAIR_TYPE_MAP[key_type] + self.keypair_config['key_size'] = int(self.keypair_config['key_size']) + + subkey_type = self.keypair_config['subkey_type'].upper() + if subkey_type not in KEYPAIR_SUBKEY_TYPE_VALID: + raise ValueError('Invalid subkey_type. {}'.format(subkey_type)) + self.keypair_config['subkey_type'] = KEYPAIR_TYPE_MAP[subkey_type] + self.keypair_config['subkey_size'] = int( + self.keypair_config['subkey_size']) + + # Make sure the keydir paths are directories and exist. + for keydir in self.keydir_config.values(): + # TODO set a strict mode here + makedirs(keydir, exist_ok=True) + + def _keyring(self, keydir): + keyfiles = [f for f in listdir(self.keydir_config[keydir]) + if isfile(f)] + return PGPKeyring(*keyfiles) + + @property + def list_keyring(self): + return self._keyring('list_keydir') - # Ensure that the homedir path is a directory before passing it to GPG. - # If it's actually a file this raises FileExistsError. - homedir_path = Path(self.gpg_config['homedir']) - homedir_path.mkdir(parents=True, exist_ok=True) + @property + def user_keyring(self): + return self._keyring('user_keydir') - super().__init__(**self.gpg_config) + @property + def archive_keyring(self): + return self._keyring('archive_keydir') diff --git a/src/mailman_pgp/pgp/inline.py b/src/mailman_pgp/pgp/inline.py new file mode 100644 index 0000000..f3b0d32 --- /dev/null +++ b/src/mailman_pgp/pgp/inline.py @@ -0,0 +1 @@ +"""""" diff --git a/src/mailman_pgp/pgp/keygen.py b/src/mailman_pgp/pgp/keygen.py index b15dbf6..e1fa6b0 100644 --- a/src/mailman_pgp/pgp/keygen.py +++ b/src/mailman_pgp/pgp/keygen.py @@ -1,29 +1,102 @@ -"""""" +"""List key generator runs in a separate process to not block for the +potentially long key generation operation.""" -import threading +import multiprocessing as mp +from os.path import exists, isfile -from mailman_pgp.config import config +from flufl.lock import Lock +from pgpy import PGPKey, PGPUID +from pgpy.constants import ( + CompressionAlgorithm, HashAlgorithm, KeyFlags, SymmetricKeyAlgorithm) -class KeyGenerator(threading.Thread): - def __init__(self, name, email, comment=None): - super().__init__(daemon=True) - self._name = name - self._comment = comment - self._email = email - self.key_fingerprint = None +class ListKeyGenerator(mp.Process): + """""" - def run(self): - default_config = config.gpg.keypair_config - key_config = dict(default_config) - key_config.update(dict(name_real=self._name, - name_email=self._email)) - if self._comment is not None: - key_config['name_comment'] = self._comment - key_input = config.gpg.gen_key_input(**key_config) - key = config.gpg.gen_key(key_input) - self.key_fingerprint = key.fingerprint + def __init__(self, keypair_config, display_name, posting_address, + request_address, queue, key_path): + super().__init__( + target=self.generate, + args=( + keypair_config, display_name, posting_address, request_address, + queue, key_path), + daemon=True) - @property - def has_key(self): - return self.key_fingerprint is not None + def generate(self, keypair_config, display_name, posting_address, + request_address, queue, key_path): + """ + + :param keypair_config: + :param display_name: + :param posting_address: + :param request_address: + :param queue: + :param key_path: + :return: + """ + if exists(key_path) and isfile(key_path): + queue.put(PGPKey.from_file(key_path)) + return + key = self._create(keypair_config, display_name, posting_address, + request_address) + self._save(key, queue, key_path) + + def _create(self, config, display_name, posting_address, request_address): + """ + + :param config: + :param display_name: + :param posting_address: + :param request_address: + :return: + """ + # Generate the Sign + Certify primary key. + key_type = config['key_type'] + key_size = config['key_size'] + key = PGPKey.new(key_type, key_size) + key_params = dict(usage={KeyFlags.Sign, KeyFlags.Certify}, + hashes=[HashAlgorithm.SHA256, + HashAlgorithm.SHA384, + HashAlgorithm.SHA512, + HashAlgorithm.SHA224], + ciphers=[SymmetricKeyAlgorithm.AES256, + SymmetricKeyAlgorithm.AES192, + SymmetricKeyAlgorithm.AES128], + compression=[CompressionAlgorithm.ZLIB, + CompressionAlgorithm.BZ2, + CompressionAlgorithm.ZIP, + CompressionAlgorithm.Uncompressed], + primary=True) + + main_uid = PGPUID.new(display_name, email=posting_address) + request_uid = PGPUID.new(display_name, + email=request_address) + + subkey_type = config['subkey_type'] + subkey_size = config['subkey_size'] + subkey = PGPKey.new(subkey_type, subkey_size) + + subkey_params = dict( + usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage}, + primary=False + ) + + key.add_uid(main_uid, **key_params) + key.add_uid(request_uid, **key_params) + key.add_subkey(subkey, **subkey_params) + return key + + def _save(self, key, queue, key_path): + """ + + :param key: + :param queue: + :param key_path: + :return: + """ + queue.put(key) + + lock = Lock(key_path) + with lock: + with open(key_path, 'w') as key_file: + key_file.write(str(key)) diff --git a/src/mailman_pgp/pgp/mime.py b/src/mailman_pgp/pgp/mime.py new file mode 100644 index 0000000..f3b0d32 --- /dev/null +++ b/src/mailman_pgp/pgp/mime.py @@ -0,0 +1 @@ +"""""" diff --git a/src/mailman_pgp/plugin.py b/src/mailman_pgp/plugin.py index d87911b..c9ee3d9 100644 --- a/src/mailman_pgp/plugin.py +++ b/src/mailman_pgp/plugin.py @@ -8,7 +8,7 @@ from zope.interface import implementer from mailman_pgp.config import config from mailman_pgp.database import Database, transaction from mailman_pgp.model.list import EncryptedMailingList -from mailman_pgp.pgp import GPG +from mailman_pgp.pgp import PGP from mailman_pgp.rest.root import RESTRoot @@ -19,7 +19,7 @@ class PGPMailman: """See `IPlugin`.""" config.load(self.name) config.db = Database() - config.gpg = GPG() + config.gpg = PGP() def post_hook(self): """See `IPlugin`.""" @@ -36,7 +36,4 @@ def on_delete(mlist): list_id=mlist.list_id).first() if encrypted_list: with transaction(): - config.gpg.delete_keys(encrypted_list.key_fingerprint, - secret=True, - subkeys=True) config.db.session.delete(encrypted_list) |
