aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJ08nY2017-06-19 18:55:44 +0200
committerJ08nY2017-06-19 18:55:44 +0200
commitc8a7543b99ffa1f28def431e9d77fd74c3f2f00f (patch)
tree35a4c8c114a8842c9171ce0acbd3370aceca75f0 /src
parent64ef87abf4599f096743081c7be721bacc255606 (diff)
downloadmailman-pgp-c8a7543b99ffa1f28def431e9d77fd74c3f2f00f.tar.gz
mailman-pgp-c8a7543b99ffa1f28def431e9d77fd74c3f2f00f.tar.zst
mailman-pgp-c8a7543b99ffa1f28def431e9d77fd74c3f2f00f.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman_pgp/config/mailman_pgp.cfg27
-rw-r--r--src/mailman_pgp/model/list.py67
-rw-r--r--src/mailman_pgp/pgp/__init__.py71
-rw-r--r--src/mailman_pgp/pgp/inline.py1
-rw-r--r--src/mailman_pgp/pgp/keygen.py119
-rw-r--r--src/mailman_pgp/pgp/mime.py1
-rw-r--r--src/mailman_pgp/plugin.py7
7 files changed, 208 insertions, 85 deletions
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)