diff options
| -rw-r--r-- | src/mailman_pgp/model/list.py | 30 | ||||
| -rw-r--r-- | src/mailman_pgp/model/sighash.py | 16 | ||||
| -rw-r--r-- | src/mailman_pgp/pgp/wrapper.py | 6 | ||||
| -rw-r--r-- | src/mailman_pgp/rules/signature.py | 25 | ||||
| -rw-r--r-- | src/mailman_pgp/rules/tests/test_signature.py | 42 | ||||
| -rw-r--r-- | src/mailman_pgp/utils/email.py | 5 | ||||
| -rw-r--r-- | src/mailman_pgp/utils/moderation.py | 3 | ||||
| -rw-r--r-- | src/mailman_pgp/utils/pgp.py | 49 | ||||
| -rw-r--r-- | src/mailman_pgp/workflows/key_change.py | 2 | ||||
| -rw-r--r-- | src/mailman_pgp/workflows/pubkey.py | 2 | ||||
| -rw-r--r-- | tox.ini | 2 |
11 files changed, 138 insertions, 44 deletions
diff --git a/src/mailman_pgp/model/list.py b/src/mailman_pgp/model/list.py index 8b2b5bc..9bee16d 100644 --- a/src/mailman_pgp/model/list.py +++ b/src/mailman_pgp/model/list.py @@ -44,36 +44,24 @@ class PGPMailingList(Base): list_id = Column(SAUnicode, index=True) # Signature related properties - unsigned_msg_action = Column(Enum(Action)) - inline_pgp_action = Column(Enum(Action)) - expired_sig_action = Column(Enum(Action)) - revoked_sig_action = Column(Enum(Action)) - # duplicate_sig_action = Column(Enum(Action)) - invalid_sig_action = Column(Enum(Action)) - strip_original_sig = Column(Boolean) - sign_outgoing = Column(Boolean) + unsigned_msg_action = Column(Enum(Action), default=Action.reject) + inline_pgp_action = Column(Enum(Action), default=Action.defer) + expired_sig_action = Column(Enum(Action), default=Action.reject) + revoked_sig_action = Column(Enum(Action), default=Action.reject) + invalid_sig_action = Column(Enum(Action), default=Action.reject) + duplicate_sig_action = Column(Enum(Action), default=Action.reject) + strip_original_sig = Column(Boolean, default=False) + sign_outgoing = Column(Boolean, default=False) # Encryption related properties - nonencrypted_msg_action = Column(Enum(Action)) + nonencrypted_msg_action = Column(Enum(Action), default=Action.reject) def __init__(self, mlist): super().__init__() self._init() - self._defaults() self.list_id = mlist.list_id self._mlist = mlist - def _defaults(self): - self.unsigned_msg_action = Action.reject - self.inline_pgp_action = Action.defer - self.expired_sig_action = Action.reject - self.revoked_sig_action = Action.reject - self.invalid_sig_action = Action.reject - self.strip_original_sig = False - self.sign_outgoing = False - - self.nonencrypted_msg_action = Action.reject - @reconstructor def _init(self): self._mlist = None diff --git a/src/mailman_pgp/model/sighash.py b/src/mailman_pgp/model/sighash.py index 2dddc02..6511484 100644 --- a/src/mailman_pgp/model/sighash.py +++ b/src/mailman_pgp/model/sighash.py @@ -16,7 +16,7 @@ # this program. If not, see <http://www.gnu.org/licenses/>. """""" -from sqlalchemy import Column, DateTime, LargeBinary, String +from sqlalchemy import Column, LargeBinary, String from mailman_pgp.model.base import Base @@ -24,19 +24,13 @@ from mailman_pgp.model.base import Base class PGPSigHash(Base): """""" - __tablename__ = "sighash" + __tablename__ = 'sighash' hash = Column(LargeBinary, primary_key=True) fingerprint = Column(String(50), index=True) - time = Column(DateTime) @staticmethod - def find(hash=None, fingerprint=None): - kws = {} - if hash is not None: - kws['hash'] = hash - if fingerprint is not None: - kws['fingerprint'] = fingerprint - if len(kws) == 0: + def hashes(hashes): + if hashes is None or len(hashes) == 0: return None - return PGPSigHash.query().filter_by(**kws).all() + return PGPSigHash.query().filter(PGPSigHash.hash.in_(hashes)).all() diff --git a/src/mailman_pgp/pgp/wrapper.py b/src/mailman_pgp/pgp/wrapper.py index f5cc8e1..cbfa7f2 100644 --- a/src/mailman_pgp/pgp/wrapper.py +++ b/src/mailman_pgp/pgp/wrapper.py @@ -20,6 +20,7 @@ from public import public from mailman_pgp.pgp.inline import InlineWrapper from mailman_pgp.pgp.mime import MIMEWrapper +from mailman_pgp.utils.pgp import verifies @public @@ -108,10 +109,7 @@ class PGPWrapper(): yield from self.inline.verify(key) def verifies(self, key): - return all(bool(verification) and - all(not sigsubj.signature.is_expired - for sigsubj in verification.good_signatures) for - verification in self.verify(key)) + return verifies(self.verify(key)) def is_encrypted(self): """ diff --git a/src/mailman_pgp/rules/signature.py b/src/mailman_pgp/rules/signature.py index 55b9b87..395dd7d 100644 --- a/src/mailman_pgp/rules/signature.py +++ b/src/mailman_pgp/rules/signature.py @@ -17,6 +17,7 @@ """Signature checking rule for the pgp-posting-chain.""" from email.utils import parseaddr +from operator import attrgetter from mailman.core.i18n import _ from mailman.interfaces.action import Action @@ -28,8 +29,10 @@ from zope.interface import implementer from mailman_pgp.model.address import PGPAddress from mailman_pgp.model.list import PGPMailingList +from mailman_pgp.model.sighash import PGPSigHash from mailman_pgp.pgp.wrapper import PGPWrapper from mailman_pgp.utils.moderation import record_action +from mailman_pgp.utils.pgp import hashes, verifies @public @@ -89,14 +92,34 @@ class Signature: 'No key set for address {}.'.format(email)) return True + if not pgp_address.key_confirmed: + record_action(msg, msgdata, Action.reject, email, + 'Key not confirmed.') + return True + + verifications = list(wrapped.verify(key)) + # Take the `invalid_sig_action` if the verification failed. - if not wrapped.verifies(key): + if not verifies(verifications): action = pgp_list.invalid_sig_action if action != Action.defer: record_action(msg, msgdata, action, email, 'Signature did not verify.') return True + sig_hashes = set(hashes(verifications)) + duplicates = set(PGPSigHash.hashes(sig_hashes)) + if duplicates: + fingerprints = map(attrgetter('fingerprint'), duplicates) + if key.fingerprint in fingerprints: + action = pgp_list.duplicate_sig_action + if action != Action.defer: + record_action(msg, msgdata, action, email, + 'Signature duplicate.') + return True + + # TODO: add the sig hashes to the db. + # XXX: we need to track key revocation separately to use it here # TODO: check key revocation here diff --git a/src/mailman_pgp/rules/tests/test_signature.py b/src/mailman_pgp/rules/tests/test_signature.py index f5c5dc3..d8d0537 100644 --- a/src/mailman_pgp/rules/tests/test_signature.py +++ b/src/mailman_pgp/rules/tests/test_signature.py @@ -28,9 +28,12 @@ from mailman_pgp.config import mm_config from mailman_pgp.database import mm_transaction, transaction from mailman_pgp.model.address import PGPAddress from mailman_pgp.model.list import PGPMailingList +from mailman_pgp.model.sighash import PGPSigHash from mailman_pgp.pgp.tests.base import load_key, load_message +from mailman_pgp.pgp.wrapper import PGPWrapper from mailman_pgp.rules.signature import Signature from mailman_pgp.testing.layers import PGPConfigLayer +from mailman_pgp.utils.pgp import hashes class TestSignatureRule(TestCase): @@ -49,10 +52,11 @@ class TestSignatureRule(TestCase): self.pgp_list = PGPMailingList.for_list(self.mlist) - sender_key = load_key('rsa_1024.pub.asc') + self.sender_key = load_key('rsa_1024.priv.asc') with transaction() as t: self.pgp_sender = PGPAddress(self.sender.preferred_address) - self.pgp_sender.key = sender_key + self.pgp_sender.key = self.sender_key.pubkey + self.pgp_sender.key_confirmed = True t.add(self.pgp_sender) self.msg_clear = load_message('clear.eml') @@ -98,7 +102,7 @@ To: test@example.com self.assertTrue(matches) self.assertAction(msgdata, Action.reject, [ 'No key set for address {}.'.format( - self.pgp_sender.address.original_email)]) + self.pgp_sender.address.original_email)]) def assertAction(self, msgdata, action, reasons): self.assertEqual(msgdata['moderation_action'], action.name) @@ -111,6 +115,7 @@ To: test@example.com self.pgp_list.expired_sig_action = Action.defer self.pgp_list.invalid_sig_action = Action.defer self.pgp_list.revoked_sig_action = Action.defer + self.pgp_list.duplicate_sig_action = Action.defer msgdata = {} matches = self.rule.check(self.mlist, self.msg_clear, msgdata) @@ -130,6 +135,7 @@ To: test@example.com self.pgp_list.expired_sig_action = Action.defer self.pgp_list.invalid_sig_action = Action.defer self.pgp_list.revoked_sig_action = Action.defer + self.pgp_list.duplicate_sig_action = Action.defer msgdata = {} matches = self.rule.check(self.mlist, self.msg_inline_signed, msgdata) @@ -146,6 +152,7 @@ To: test@example.com self.pgp_list.expired_sig_action = Action.defer self.pgp_list.invalid_sig_action = Action.hold self.pgp_list.revoked_sig_action = Action.defer + self.pgp_list.duplicate_sig_action = Action.defer msgdata = {} matches = self.rule.check(self.mlist, self.msg_inline_signed_invalid, @@ -158,3 +165,32 @@ To: test@example.com msgdata) self.assertTrue(matches) self.assertAction(msgdata, Action.hold, ['Signature did not verify.']) + + def test_duplicate_sig_action(self): + with transaction() as t: + self.pgp_list.unsigned_msg_action = Action.defer + self.pgp_list.inline_pgp_action = Action.defer + self.pgp_list.expired_sig_action = Action.defer + self.pgp_list.invalid_sig_action = Action.defer + self.pgp_list.revoked_sig_action = Action.defer + self.pgp_list.duplicate_sig_action = Action.hold + + wrapped = PGPWrapper(self.msg_mime_signed) + sig_hashes = set(hashes(wrapped.verify(self.sender_key.pubkey))) + wrapped = PGPWrapper(self.msg_inline_signed) + sig_hashes |= set(hashes(wrapped.verify(self.sender_key.pubkey))) + for hash in sig_hashes: + sig_hash = PGPSigHash() + sig_hash.hash = hash + sig_hash.fingerprint = self.sender_key.pubkey.fingerprint + t.add(sig_hash) + + msgdata = {} + matches = self.rule.check(self.mlist, self.msg_mime_signed, msgdata) + self.assertTrue(matches) + self.assertAction(msgdata, Action.hold, ['Signature duplicate.']) + + msgdata = {} + matches = self.rule.check(self.mlist, self.msg_inline_signed, msgdata) + self.assertTrue(matches) + self.assertAction(msgdata, Action.hold, ['Signature duplicate.']) diff --git a/src/mailman_pgp/utils/email.py b/src/mailman_pgp/utils/email.py index 4d08d8a..4541313 100644 --- a/src/mailman_pgp/utils/email.py +++ b/src/mailman_pgp/utils/email.py @@ -16,7 +16,10 @@ # this program. If not, see <http://www.gnu.org/licenses/>. """""" +from public import public + +@public def copy_headers(from_msg, to_msg, overwrite=False): """ Copy the headers and unixfrom from a message to another one. @@ -32,4 +35,4 @@ def copy_headers(from_msg, to_msg, overwrite=False): if key not in to_msg: to_msg[key] = value if to_msg.get_unixfrom() is None: - to_msg.set_unixfrom(from_msg.get_unixfrom())
\ No newline at end of file + to_msg.set_unixfrom(from_msg.get_unixfrom()) diff --git a/src/mailman_pgp/utils/moderation.py b/src/mailman_pgp/utils/moderation.py index a27ec48..850f885 100644 --- a/src/mailman_pgp/utils/moderation.py +++ b/src/mailman_pgp/utils/moderation.py @@ -16,7 +16,10 @@ # this program. If not, see <http://www.gnu.org/licenses/>. """""" +from public import public + +@public def record_action(msg, msgdata, action, sender, reason): """ diff --git a/src/mailman_pgp/utils/pgp.py b/src/mailman_pgp/utils/pgp.py new file mode 100644 index 0000000..7b2f958 --- /dev/null +++ b/src/mailman_pgp/utils/pgp.py @@ -0,0 +1,49 @@ +# Copyright (C) 2017 Jan Jancar +# +# This file is a part of the Mailman PGP plugin. +# +# 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/>. + +"""Miscellaneous PGP utilities.""" +from public import public + + +@public +def verifies(verifications): + """ + + :param verifications: + :type verifications: Sequence[pgpy.types.SignatureVerification] + :return: bool + """ + return all(bool(verification) and + all(not sigsubj.signature.is_expired + for sigsubj in verification.good_signatures) for + verification in verifications) + + +@public +def hashes(verifications): + """ + + :param verifications: + :return: + :rtype: Generator[str] + """ + for verification in verifications: + for sigsubj in verification.good_signatures: + data = sigsubj.signature.hashdata(sigsubj.subject) + hasher = sigsubj.signature.hash_algorithm.hasher + hasher.update(data) + yield hasher.digest() diff --git a/src/mailman_pgp/workflows/key_change.py b/src/mailman_pgp/workflows/key_change.py index 50a6c5b..01693f5 100644 --- a/src/mailman_pgp/workflows/key_change.py +++ b/src/mailman_pgp/workflows/key_change.py @@ -29,8 +29,8 @@ from zope.interface import implementer from mailman_pgp.database import transaction from mailman_pgp.model.address import PGPAddress from mailman_pgp.model.list import PGPMailingList -from mailman_pgp.utils.email import copy_headers from mailman_pgp.pgp.wrapper import PGPWrapper +from mailman_pgp.utils.email import copy_headers CHANGE_CONFIRM_REQUEST = """\ ---------- diff --git a/src/mailman_pgp/workflows/pubkey.py b/src/mailman_pgp/workflows/pubkey.py index 8196eef..a13d491 100644 --- a/src/mailman_pgp/workflows/pubkey.py +++ b/src/mailman_pgp/workflows/pubkey.py @@ -5,8 +5,8 @@ from pgpy import PGPKey from mailman_pgp.database import transaction from mailman_pgp.model.address import PGPAddress from mailman_pgp.model.list import PGPMailingList -from mailman_pgp.utils.email import copy_headers from mailman_pgp.pgp.wrapper import PGPWrapper +from mailman_pgp.utils.email import copy_headers KEY_REQUEST = """\ ---------- @@ -14,7 +14,7 @@ deps = flufl.testing dev: -e../mailman dev: -e../PGPy setenv = MAILMAN_EXTRA_TESTING_CFG = {toxinidir}/src/mailman_pgp/config/mailman.cfg - cov: COVERAGE_PROCESS_START={[coverage]rcfile} + cov: COVERAGE_PROCESS_START = {[coverage]rcfile} commands = nocov: python -m nose2 -v {posargs} cov: python -m coverage run {[coverage]rc} -m nose2 -v {posargs} cov: python -m coverage combine {[coverage]rc} |
