diff options
| author | J08nY | 2017-06-09 17:41:24 +0200 |
|---|---|---|
| committer | J08nY | 2017-06-09 17:41:24 +0200 |
| commit | b932ba4a32f208aa934bad8b4039e8c871b6715f (patch) | |
| tree | c93f1277353928446dccaa4e16845fe9a83372b5 /src/mailman_pgp | |
| parent | 25487795779c05ff8e97680550948443924b98c0 (diff) | |
| download | mailman-pgp-b932ba4a32f208aa934bad8b4039e8c871b6715f.tar.gz mailman-pgp-b932ba4a32f208aa934bad8b4039e8c871b6715f.tar.zst mailman-pgp-b932ba4a32f208aa934bad8b4039e8c871b6715f.zip | |
Diffstat (limited to 'src/mailman_pgp')
27 files changed, 447 insertions, 0 deletions
diff --git a/src/mailman_pgp/__init__.py b/src/mailman_pgp/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/mailman_pgp/__init__.py diff --git a/src/mailman_pgp/archivers/__init__.py b/src/mailman_pgp/archivers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/mailman_pgp/archivers/__init__.py diff --git a/src/mailman_pgp/archivers/local.py b/src/mailman_pgp/archivers/local.py new file mode 100644 index 0000000..43e08ee --- /dev/null +++ b/src/mailman_pgp/archivers/local.py @@ -0,0 +1,14 @@ +""" +Archives messages locally, encrypted (TBD how), +similar to Mailman's prototype archiver. +""" + +from mailman.interfaces.archiver import IArchiver +from public import public +from zope.interface import implementer + + +@public +@implementer(IArchiver) +class LocalArchiver: + pass diff --git a/src/mailman_pgp/archivers/remote.py b/src/mailman_pgp/archivers/remote.py new file mode 100644 index 0000000..388de3a --- /dev/null +++ b/src/mailman_pgp/archivers/remote.py @@ -0,0 +1,14 @@ +""" +Archives messages by sending to django-pgpmailman, +an extension on top of Postorius and HyperKitty. +""" + +from mailman.interfaces.archiver import IArchiver +from public import public +from zope.interface import implementer + + +@public +@implementer(IArchiver) +class RemoteArchiver: + pass diff --git a/src/mailman_pgp/commands/__init__.py b/src/mailman_pgp/commands/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/mailman_pgp/commands/__init__.py diff --git a/src/mailman_pgp/commands/eml_key.py b/src/mailman_pgp/commands/eml_key.py new file mode 100644 index 0000000..03c0877 --- /dev/null +++ b/src/mailman_pgp/commands/eml_key.py @@ -0,0 +1,36 @@ +"""The key email command.""" + +from mailman.interfaces.command import ContinueProcessing, IEmailCommand +from public import public +from zope.interface import implementer + + +@public +@implementer(IEmailCommand) +class KeyCommand: + name = 'key' + argument_description = '<change|revoke|sign>' + short_description = '' + description = '' + + def process(self, mlist, msg, msgdata, arguments, results): + """See `IEmailCommand`.""" + if len(arguments) == 0: + print('No sub-command specified,' + ' must be one of <change|revoke|sign>.', file=results) + return ContinueProcessing.no + if arguments[0] == 'change': + # New public key in attachment, requires to be signed with current + # key + pass + elif arguments[0] == 'revoke': + # Current key revocation certificate in attachment, restarts the + # subscription process, or rather only it's key setup part. + pass + elif arguments[0] == 'sign': + # List public key attached, signed by the users current key. + pass + else: + print('Wrong sub-command specified,' + ' must be one of <change|revoke|sign>.', file=results) + return ContinueProcessing.no diff --git a/src/mailman_pgp/config/__init__.py b/src/mailman_pgp/config/__init__.py new file mode 100644 index 0000000..a6f7004 --- /dev/null +++ b/src/mailman_pgp/config/__init__.py @@ -0,0 +1,8 @@ +"""""" + +from configparser import ConfigParser + +from public.public import public + +config = ConfigParser() +public(config=config) diff --git a/src/mailman_pgp/config/mailman.cfg b/src/mailman_pgp/config/mailman.cfg new file mode 100644 index 0000000..7bd45a7 --- /dev/null +++ b/src/mailman_pgp/config/mailman.cfg @@ -0,0 +1,20 @@ + +# Example additions to mailman.cfg to enable PGP + +[plugin.pgp] +class: mailman_pgp.plugin.PGPMailman +path: mailman_pgp +enable: yes +configuration: python:mailman_pgp.config.pgpmailman + +[runner.in] +class: mailman_pgp.runners.incoming.IncomingRunner + +[runner.in_default] +class: mailman.runners.incoming.IncomingRunner + +[runner.out] +class: mailman_pgp.runners.outgoing.OutgoingRunner + +[runner.out_default] +class: mailman.runners.outgoing.OutgoingRunner diff --git a/src/mailman_pgp/config/pgpmailman.cfg b/src/mailman_pgp/config/pgpmailman.cfg new file mode 100644 index 0000000..45aa7b0 --- /dev/null +++ b/src/mailman_pgp/config/pgpmailman.cfg @@ -0,0 +1,16 @@ +# Default PGP config + +[db] +# db path the PGP plugin will use to store list/user configuration (not keys!). +url = sqlite:////$DATA_DIR/pgp.db + +[keyrings] +# Keyring used to store list keypairs. +core = $DATA_DIR/pgp_core.gpp + +# Keyring used to store user public keys. +users = $DATA_DIR/pgp_users.gpg + +[queues] +in = in_default +out = out_default
\ No newline at end of file diff --git a/src/mailman_pgp/database/__init__.py b/src/mailman_pgp/database/__init__.py new file mode 100644 index 0000000..b799942 --- /dev/null +++ b/src/mailman_pgp/database/__init__.py @@ -0,0 +1,36 @@ +"""""" + +from contextlib import contextmanager + +from mailman.config import config as mailman_config +from mailman.utilities.string import expand +from public import public +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from mailman_pgp.config import config +from mailman_pgp.model.base import Base + + +@public +class Database: + def __init__(self): + url = config.get('db', 'url') + self.url = expand(url, None, mailman_config.paths) + self.engine = create_engine(self.url) + Session = sessionmaker(bind=self.engine) + self.session = Session() + Base.metadata.create_all(self.engine) + self.session.commit() + + +@public +@contextmanager +def transaction(): + try: + yield + except: + config.db.session.abort() + raise + else: + config.db.session.commit() diff --git a/src/mailman_pgp/model/__init__.py b/src/mailman_pgp/model/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/mailman_pgp/model/__init__.py diff --git a/src/mailman_pgp/model/base.py b/src/mailman_pgp/model/base.py new file mode 100644 index 0000000..e25407f --- /dev/null +++ b/src/mailman_pgp/model/base.py @@ -0,0 +1,8 @@ +"""""" + +from public import public +from sqlalchemy.ext.declarative import declarative_base + + +Base = declarative_base() +public(Base=Base) diff --git a/src/mailman_pgp/model/list.py b/src/mailman_pgp/model/list.py new file mode 100644 index 0000000..f781a9a --- /dev/null +++ b/src/mailman_pgp/model/list.py @@ -0,0 +1,24 @@ +"""""" + +from mailman.database.types import Enum, SAUnicode +from mailman.interfaces.action import Action +from public import public +from sqlalchemy import Boolean, Column, Integer + +from mailman_pgp.model.base import Base + + +@public +class EncryptedMailingList(Base): + __tablename__ = 'encrypted_lists' + + id = Column(Integer, primary_key=True) + list_id = Column(SAUnicode) + key_fingerprint = Column(SAUnicode) + unsigned_msg_action = Column(Enum(Action)) + nonencrypted_msg_action = Column(Enum(Action)) + strip_original_signature = Column(Boolean) + sign_outgoing = Column(Boolean) + + def __init__(self, mlist): + self.list_id = mlist.list_id diff --git a/src/mailman_pgp/pgp/__init__.py b/src/mailman_pgp/pgp/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/mailman_pgp/pgp/__init__.py diff --git a/src/mailman_pgp/pgp/keyrings.py b/src/mailman_pgp/pgp/keyrings.py new file mode 100644 index 0000000..d4fd4b4 --- /dev/null +++ b/src/mailman_pgp/pgp/keyrings.py @@ -0,0 +1,3 @@ + +from public import public + diff --git a/src/mailman_pgp/plugin.py b/src/mailman_pgp/plugin.py new file mode 100644 index 0000000..e6eec4f --- /dev/null +++ b/src/mailman_pgp/plugin.py @@ -0,0 +1,42 @@ +"""A PGP plugin for GNU Mailman.""" +from mailman.app import events +from mailman.config import config as mailman_config +from mailman.interfaces.listmanager import ListDeletedEvent +from mailman.interfaces.plugin import IPlugin +from mailman.utilities.modules import expand_path +from public import public +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.rest.root import RESTRoot + + +@public +@implementer(IPlugin) +class PGPMailman: + def pre_hook(self): + """See `IPlugin`.""" + config.read( + expand_path( + dict(mailman_config.plugin_configs)[self.name].configuration)) + config.db = Database() + config.name = self.name + + def post_hook(self): + """See `IPlugin`.""" + pass + + def rest_object(self): + """See `IPlugin`.""" + return RESTRoot() + + +@events.subscribe(ListDeletedEvent) +def on_delete(mlist): + encrypted_list = config.db.session.query(EncryptedMailingList).filter_by( + list_id=mlist.list_id).first() + if encrypted_list: + with transaction(): + config.db.session.delete(encrypted_list) diff --git a/src/mailman_pgp/rest/__init__.py b/src/mailman_pgp/rest/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/mailman_pgp/rest/__init__.py diff --git a/src/mailman_pgp/rest/lists.py b/src/mailman_pgp/rest/lists.py new file mode 100644 index 0000000..e0d2b0f --- /dev/null +++ b/src/mailman_pgp/rest/lists.py @@ -0,0 +1,54 @@ +"""""" + +from mailman.rest.helpers import ( + child, CollectionMixin, etag, not_found, NotFound, okay) +from public import public + +from mailman_pgp.config import config +from mailman_pgp.model.list import EncryptedMailingList + + +class _EncryptedBase(CollectionMixin): + def _resource_as_dict(self, emlist): + """See `CollectionMixin`.""" + return dict(list_id=emlist.list_id, + key_fingerprint=emlist.key_fingerprint, + unsigned_msg_action=emlist.unsigned_msg_action, + nonencrypted_msg_action=emlist.nonencrypted_msg_action, + strip_original_signature=emlist.strip_original_signature, + sign_outgoing=emlist.sign_outgoing, + self_link=self.api.path_to( + '/plugins/{}/lists/{}'.format(config.name, + emlist.list_id))) + + def _get_collection(self, request): + """See `CollectionMixin`.""" + return config.db.session.query(EncryptedMailingList).all() + + +@public +class AllEncryptedLists(_EncryptedBase): + def on_get(self, request, response): + """/lists""" + resource = self._make_collection(response) + return okay(response, etag(resource)) + + +@public +class AnEncryptedList(_EncryptedBase): + def __init__(self, list_id): + self._mlist = config.db.session.query(EncryptedMailingList).filter_by( + list_id=list_id).first() + + def on_get(self, request, response): + if self._mlist is None: + return not_found() + else: + okay(response, self._resource_as_json(self._mlist)) + + @child() + def key(self, context, segments): + if self._mlist is None: + return NotFound(), [] + else: + pass diff --git a/src/mailman_pgp/rest/root.py b/src/mailman_pgp/rest/root.py new file mode 100644 index 0000000..286d9ae --- /dev/null +++ b/src/mailman_pgp/rest/root.py @@ -0,0 +1,39 @@ +""" +REST root. + + +/lists/ -> List all known encrypted lists. +/lists/<list_id>/ -> +/lists/<list_id>/key -> GET list_public_key +/lists/<list_id>/archive/key -> GET/POST list_archive_public_key + +/users/ -> List all known users of encrypted lists. +/users/<uid>/ -> +/users/<uid>/key -> GET/POST user_public_key + +""" + +from mailman.rest.helpers import child +from public import public + +from mailman_pgp.rest.lists import AllEncryptedLists, AnEncryptedList +from mailman_pgp.rest.users import AllUsers, AUser + + +@public +class RESTRoot: + @child() + def lists(self, context, segments): + if len(segments) == 0: + return AllEncryptedLists(), [] + else: + list_id = segments.pop(0) + return AnEncryptedList(list_id), segments + + @child() + def users(self, context, segments): + if len(segments) == 0: + return AllUsers(), [] + else: + uid = segments.pop(0) + return AUser(uid), segments diff --git a/src/mailman_pgp/rest/users.py b/src/mailman_pgp/rest/users.py new file mode 100644 index 0000000..09990f6 --- /dev/null +++ b/src/mailman_pgp/rest/users.py @@ -0,0 +1,13 @@ +"""""" + +from public import public + + +@public +class AllUsers: + pass + + +@public +class AUser: + pass diff --git a/src/mailman_pgp/runners/__init__.py b/src/mailman_pgp/runners/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/mailman_pgp/runners/__init__.py diff --git a/src/mailman_pgp/runners/incoming.py b/src/mailman_pgp/runners/incoming.py new file mode 100644 index 0000000..f13b971 --- /dev/null +++ b/src/mailman_pgp/runners/incoming.py @@ -0,0 +1,39 @@ +"""The encryption-aware incoming runner.""" + +from mailman.config import config as mailman_config +from mailman.core.runner import Runner +from mailman.email.message import Message +from mailman.model.mailinglist import MailingList +from public import public + +from mailman_pgp.config import config +from mailman_pgp.model.list import EncryptedMailingList + + +@public +class IncomingRunner(Runner): + def _dispose(self, mlist: MailingList, msg: Message, msgdata: dict): + """See `IRunner`.""" + # Is the message for an encrypted mailing list? If not, pass to default + # incoming runner. If yes, go on. + encrypted_list = config.db.query(EncryptedMailingList).filter_by( + list_id=mlist.list_id).first() + if not encrypted_list: + inq = config.get('queues', 'in') + mailman_config.switchboards[inq].enqueue(msg, msgdata, + listid=mlist.list_id) + return False + # Is the message encrypted? + if msg.get_content_type() == 'multipart/signed' and msg.get_param( + 'protocol') == 'application/pgp-signature': + # only signed. + pass + elif msg.get_content_type() == 'multipart/encrypted' and msg.get_param( + 'protocol') == 'application/pgp-encrypted': + # definitely encrypted, might still be signed + pass + else: + # not encrypted or signed + pass + from email.iterators import _structure + _structure(msg) diff --git a/src/mailman_pgp/runners/outgoing.py b/src/mailman_pgp/runners/outgoing.py new file mode 100644 index 0000000..2723b17 --- /dev/null +++ b/src/mailman_pgp/runners/outgoing.py @@ -0,0 +1,24 @@ +"""The encryption-aware outgoing runner""" + +from mailman.config import config as mailman_config +from mailman.core.runner import Runner +from mailman.email.message import Message +from mailman.model.mailinglist import MailingList +from public import public + +from mailman_pgp.config import config +from mailman_pgp.model.list import EncryptedMailingList + + +@public +class OutgoingRunner(Runner): + def _dispose(self, mlist: MailingList, msg: Message, msgdata: dict): + """See `IRunner`.""" + encrypted_list = config.db.query(EncryptedMailingList).filter_by( + list_id=mlist.list_id).first() + if not encrypted_list: + outq = config.get('queues', 'out') + mailman_config.switchboards[outq].enqueue(msg, + msgdata, + listid=mlist.list_id) + return False diff --git a/src/mailman_pgp/styles/__init__.py b/src/mailman_pgp/styles/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/mailman_pgp/styles/__init__.py diff --git a/src/mailman_pgp/styles/announce.py b/src/mailman_pgp/styles/announce.py new file mode 100644 index 0000000..b4eae80 --- /dev/null +++ b/src/mailman_pgp/styles/announce.py @@ -0,0 +1,17 @@ +"""""" + +from mailman.styles.default import LegacyAnnounceOnly +from public import public + +from mailman_pgp.styles.base import EncryptedStyle + + +@public +class AnnounceStyle(LegacyAnnounceOnly, EncryptedStyle): + name = 'encrypted-announce' + description = 'Announce only encrypted mailing list style.' + + def apply(self, mailing_list): + """See `IStyle`.""" + LegacyAnnounceOnly.apply(self, mailing_list) + EncryptedStyle.apply(self, mailing_list) diff --git a/src/mailman_pgp/styles/base.py b/src/mailman_pgp/styles/base.py new file mode 100644 index 0000000..36d11c7 --- /dev/null +++ b/src/mailman_pgp/styles/base.py @@ -0,0 +1,23 @@ +"""""" + +from public import public + +from mailman_pgp.config import config +from mailman_pgp.database import transaction +from mailman_pgp.model.list import EncryptedMailingList + + +@public +class EncryptedStyle: + def apply(self, mailing_list): + """Creates the encrypted mailing list instance for the list it's + applied to. + """ + enc_list = config.db.session.query(EncryptedMailingList).filter_by( + list_id=mailing_list.list_id).first() + if enc_list: + return + + enc_list = EncryptedMailingList(mailing_list) + with transaction(): + config.db.session.add(enc_list) diff --git a/src/mailman_pgp/styles/discussion.py b/src/mailman_pgp/styles/discussion.py new file mode 100644 index 0000000..f9db7a7 --- /dev/null +++ b/src/mailman_pgp/styles/discussion.py @@ -0,0 +1,17 @@ +"""""" + +from mailman.styles.default import LegacyDefaultStyle +from public import public + +from mailman_pgp.styles.base import EncryptedStyle + + +@public +class DiscussionStyle(LegacyDefaultStyle, EncryptedStyle): + name = 'encrypted-default' + description = 'Ordinary discussion encrypted mailing list style.' + + def apply(self, mailing_list): + """See `IStyle`.""" + LegacyDefaultStyle.apply(self, mailing_list) + EncryptedStyle.apply(self, mailing_list) |
