piknik

Unnamed repository; edit this file 'description' to name the repository.
Info | Log | Files | Refs | README | LICENSE

commit 67c4e43941054267f6e9513448481cd4de0519f6
parent 9a655f448de6877ef7cd0a8cae2968a24a071165
Author: lash <dev@holbrook.no>
Date:   Tue, 15 Nov 2022 14:29:38 +0000

Visitor pattern for message verifier

Diffstat:
MCHANGELOG | 3+++
MROADMAP | 2--
Mpiknik/basket.py | 11+++++------
Mpiknik/crypto.py | 74++++++++++++++++++++++++++++++--------------------------------------------
Mpiknik/error.py | 4++++
Mpiknik/msg.py | 29++++++++++++++++++++++++++---
Mpiknik/runnable/show.py | 4++++
Mtests/test_crypto.py | 13+++++++------
Mtests/test_msg.py | 22++++++++++++++++++++++
Mtests/test_store.py | 4++--
10 files changed, 103 insertions(+), 63 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -1,3 +1,6 @@ +- 0.2.0 + * GPG signing of issue messages. + * Use visitor pattern for messages verifier - 0.1.3 * Add command for adding MIME Multipart comments - 0.1.2 diff --git a/ROADMAP b/ROADMAP @@ -9,5 +9,3 @@ - target hash above - git tag - arbitrary string identifier -- 0.2.0 - * GPG signing of issue messages. diff --git a/piknik/basket.py b/piknik/basket.py @@ -14,7 +14,7 @@ logg = logging.getLogger(__name__) class Basket: - def __init__(self, state_factory, message_wrapper=None, message_verifier=None): + def __init__(self, state_factory, message_wrapper=None): self.no_resurrect = True self.state = state_factory.create_states(default_state='proposed', verifier=self.__check_resurrect) self.state.add('backlog') @@ -33,7 +33,6 @@ class Basket: self.__msg = state_factory.create_messages() self.__msg_wrap = message_wrapper - self.__msg_verify = message_verifier self.issues_rev = {} @@ -142,12 +141,12 @@ class Basket: return shep.state.split_elements(r) - def __get_msg(self, issue_id): + def __get_msg(self, issue_id, envelope_callback=None, message_callback=None): r = self.state.get(issue_id) o = Issue.from_str(r) try: v = self.__msg.get(issue_id) - m = IssueMessage.parse(o, v.decode('utf-8'), verifier=self.__msg_verify) + m = IssueMessage.parse(o, v.decode('utf-8'), envelope_callback=envelope_callback, message_callback=message_callback) return m except FileNotFoundError: logg.debug('instantiating new message log for {}'.format(issue_id)) @@ -155,8 +154,8 @@ class Basket: return IssueMessage(o) - def get_msg(self, issue_id): - return self.__get_msg(issue_id) + def get_msg(self, issue_id, envelope_callback=None, message_callback=None): + return self.__get_msg(issue_id, envelope_callback=envelope_callback, message_callback=message_callback) def assign(self, issue_id, identity): diff --git a/piknik/crypto.py b/piknik/crypto.py @@ -7,20 +7,13 @@ from email.message import Message # external imports import gnupg -logging.basicConfig(level=logging.DEBUG) +# local imports +from piknik.error import VerifyError + logg = logging.getLogger() logging.getLogger('gnupg').setLevel(logging.ERROR) - -class CorruptEnvelope(Exception): - pass - - -class InvalidSignature(Exception): - pass - - class PGPSigner: def __init__(self, home_dir=None, default_key=None, passphrase=None, use_agent=False): @@ -28,6 +21,8 @@ class PGPSigner: self.default_key = default_key self.passphrase = passphrase self.use_agent = use_agent + self.__envelope_state = -1 # -1 not in envelope, 0 in outer envelope, 1 inner envelope, not (yet) valid, 2 envelope valid (with signature) + self.__envelope = None def sign(self, msg, passphrase=None): # msg = IssueMessage object @@ -48,10 +43,28 @@ class PGPSigner: return m - - def __verify_msg(self, m, ms): - v = m.as_string() - sig = ms.get_payload() + + def envelope_callback(self, msg, env_header): + self.__envelope = None + if env_header != 'pgp': + raise VerifyError('expected envelope type "pgp", but got {}'.format(env_header)) + if self.__envelope_state > -1 and self.__envelope_state < 2: + raise VerifyError('new envelope before previous was verified') + self.__envelope = msg + self.__envelope_state = 0 + + + def message_callback(self, envelope, msg, message_id): + if msg.get('Content-Type') != 'application/pgp-signature': + return + + if self.__envelope_state == 0: + self.__envelope_state = 1 + self.__envelope = msg + return + + v = self.__envelope.as_string() + sig = msg.get_payload() (fd, fp) = tempfile.mkstemp() f = os.fdopen(fd, 'w') f.write(sig) @@ -59,35 +72,8 @@ class PGPSigner: r = self.gpg.verify_data(fp, v.encode('utf-8')) os.unlink(fp) if r.key_status != None: - raise InvalidSignature('key status {}'.format(r.key_status)) + raise VerifyError('unexpeced key status {}'.format(r.key_status)) if r.status != 'signature valid': - raise InvalidSignature('invalid signature') + raise VerifyError('invalid signature') logg.debug('signature ok') - - - def verify(self, msg): # msg = IssueMessage object - in_envelope = False - message_ids = [] - envelope_message = None - message_id = None - for m in msg.walk(): - if m.get('X-Piknik-Envelope') == 'pgp': - logg.debug('detected pgp envelope') - in_envelope = True - elif in_envelope: - if envelope_message != None: - if m.get('X-Piknik-Envelope') != None: - raise CorruptEnvelope() - if m.get('Content-Type') == 'application/pgp-signature': - self.__verify_msg(envelope_message, m) - logg.debug('pgp signature for message id "{}" ok'.format(message_id)) - message_ids.append(message_id) - - in_envelope = False - envelope_message = None - message_id = None - else: - message_id = m.get('X-Piknik-Msg-Id') - logg.debug('checking envelope for message id "{}"'.format(message_id)) - envelope_message = m - return message_ids + self.__envelope_state = 2 diff --git a/piknik/error.py b/piknik/error.py @@ -8,3 +8,7 @@ class AlreadyAssignedError(Exception): class UnknownIdentityError(Exception): pass + + +class VerifyError(Exception): + pass diff --git a/piknik/msg.py b/piknik/msg.py @@ -28,12 +28,35 @@ class IssueMessage: self.__m.set_boundary(str(uuid.uuid4())) + def __unwrap(self, msg, envelope_callback=None, message_callback=None): + message_ids = [] + message_id = None + envelope = None + for m in msg.walk(): + env_header = m.get('X-Piknik-Envelope') + if env_header != None: + if envelope_callback != None: + envelope_callback(m, env_header) + envelope = m + continue + + if message_callback == None: + continue + + new_message_id = m.get('X-Piknik-Msg-Id') + if new_message_id != None: + message_id = new_message_id + message_ids.append(message_id) + + message_callback(envelope, m, message_id) + return message_ids + + @classmethod - def parse(cls, issue, v, verifier=None): + def parse(cls, issue, v, envelope_callback=None, message_callback=None): o = cls(issue) m = message_from_string(v) - if verifier != None: - verifier(m) + o.__unwrap(m, envelope_callback=envelope_callback, message_callback=message_callback) o.__m = m return o diff --git a/piknik/runnable/show.py b/piknik/runnable/show.py @@ -42,6 +42,10 @@ tags: {} s += ' (owner)' print('\t' + str(s)) + m = basket.get_msg(arg.issue_id) + print() + print(m) + def main(): o = basket.get(arg.issue_id) diff --git a/tests/test_crypto.py b/tests/test_crypto.py @@ -9,6 +9,7 @@ from email.message import Message # local imports from piknik import Basket from piknik import Issue +from piknik.msg import IssueMessage # test imports from tests.common import TestStates @@ -26,7 +27,7 @@ class TestMsg(unittest.TestCase): def setUp(self): self.store = TestStates() (self.crypto, self.gpg, self.gpg_dir) = pgp_setup() - self.b = Basket(self.store, message_wrapper=self.crypto.sign, message_verifier=self.crypto.verify) + self.b = Basket(self.store, message_wrapper=self.crypto.sign) def tearDown(self): @@ -48,8 +49,9 @@ class TestMsg(unittest.TestCase): two.set_payload('bar') m.attach(two) + o = Issue('foo') m = self.crypto.sign(m, passphrase='foo') - self.crypto.verify(m) + r = IssueMessage.parse(o, str(m), envelope_callback=self.crypto.envelope_callback, message_callback=self.crypto.message_callback) def test_wrap_double_sig(self): @@ -93,10 +95,9 @@ class TestMsg(unittest.TestCase): m = self.crypto.sign(m, passphrase='foo') mp.attach(m) - r = self.crypto.verify(mp) - self.assertEqual(len(r), 2) - self.assertIn('foo', r) - self.assertIn('bar', r) + self.crypto.envelope_callback(mp, 'pgp') + r = self.crypto.message_callback(mp, m, 'foo') + r = self.crypto.message_callback(mp, m, 'bar') # TODO: assert diff --git a/tests/test_msg.py b/tests/test_msg.py @@ -27,6 +27,12 @@ def test_wrapper(p): return m +def test_unwrapper(msg, message_callback=None, part_callback=None): + for v in msg.walk(): + if message_callback != None: + message_callback(v) + + class TestMsg(unittest.TestCase): def setUp(self): @@ -74,5 +80,21 @@ class TestMsg(unittest.TestCase): print(m) + def test_render(self): + b = Basket(self.store, message_wrapper=test_wrapper) + o = Issue('bar') + v = b.add(o) + m = b.msg(v, 's:foo') + m = b.msg(v, 's:bar', 's:baz') + + def render_envelope(msg, hdr): + print('eeeeeenvvv {} {}'.format(hdr, msg)) + + def render_message(envelope, msg, mid): + print('rendeeeer {} {}'.format(mid, msg)) + + m = b.get_msg(v, envelope_callback=render_envelope, message_callback=render_message) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_store.py b/tests/test_store.py @@ -100,12 +100,12 @@ class TestStore(unittest.TestCase): def test_msg_sig_verify_resume(self): (crypto, gpg, gpg_dir) = pgp_setup() - b = Basket(self.store_factory, message_wrapper=crypto.sign, message_verifier=crypto.verify) + b = Basket(self.store_factory, message_wrapper=crypto.sign) o = Issue('foo') v = b.add(o) r = b.msg(v, 's:foo', 's:bar') - b = Basket(self.store_factory, message_wrapper=crypto.sign, message_verifier=crypto.verify) + b = Basket(self.store_factory, message_wrapper=crypto.sign) m = b.get_msg(v)