commit 67c4e43941054267f6e9513448481cd4de0519f6
parent 9a655f448de6877ef7cd0a8cae2968a24a071165
Author: lash <dev@holbrook.no>
Date: Tue, 15 Nov 2022 14:29:38 +0000
Visitor pattern for message verifier
Diffstat:
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)