diff --git a/docs/api.md b/docs/api.md index 88eede7..67ae8ac 100644 --- a/docs/api.md +++ b/docs/api.md @@ -286,11 +286,17 @@ the rest of the protocol to proceed. If they do not match, then the two programs are not talking to each other (they may both be talking to a man-in-the-middle attacker), and the protocol should be abandoned. -Once retrieved, you can turn this into hex or Base64 to print it, or render -it as ASCII-art, etc. Once the users are convinced that `verify()` from both -sides are the same, call `send()` to continue the protocol. If you call -`send()` before `verify()`, it will perform the complete protocol without -pausing. +Deferred-mode applications can wait for `d=w.when_verified()`: the Deferred +it returns will fire with the verifier. You can turn this into hex or Base64 +to print it, or render it as ASCII-art, etc. + +Asking the wormhole object for the verifier does not affect the flow of the +protocol. To benefit from verification, applications must refrain from +sending any data (with `w.send(data)`) until after the verifiers are approved +by the user. In addition, applications must queue or otherwise ignore +incoming (received) messages until that point. However once the verifiers are +confirmed, previously-received messages can be considered valid and processed +as usual. ## Welcome Messages @@ -377,6 +383,14 @@ those Deferreds. has been told `h.set_words()`, or immediately after `w.set_code(code)` is called. This is most useful after calling `w.generate_code()`, to show the generated code to the user so they can transcribe it to their peer. +* key (`yield w.when_key()` / `dg.wormhole_key()`): fired when the + key-exchange process has completed and a purported shared key is + established. At this point we do not know that anyone else actually shares + this key: the peer may have used the wrong code, or may have disappeared + altogether. To wait for proof that the key is shared, wait for + `when_verified` instead. This event is really only useful for detecting + that the initiating peer has disconnected after leaving the initial PAKE + message, to display a pacifying message to the user. * verified (`verifier = yield w.when_verified()` / `dg.wormhole_verified(verifier)`: fired when the key-exchange process has completed and a valid VERSION message has arrived. The "verifier" is a byte diff --git a/docs/state-machines/boss.dot b/docs/state-machines/boss.dot index 866f0e8..8e32899 100644 --- a/docs/state-machines/boss.dot +++ b/docs/state-machines/boss.dot @@ -73,7 +73,7 @@ digraph { {rank=same; Other S_closed} Other [shape="box" style="dashed" - label="rx_welcome -> process (maybe rx_unwelcome)\nsend -> S.send\ngot_message -> got_version or got_phase\ngot_key -> W.got_key\ngot_verifier -> W.got_verifier\nallocate_Code -> C.allocate_code\ninput_code -> C.input_code\nset_code -> C.set_code" + label="rx_welcome -> process (maybe rx_unwelcome)\nsend -> S.send\ngot_message -> got_version or got_phase\ngot_key -> W.got_key\ngot_verifier -> W.got_verifier\nallocate_code -> C.allocate_code\ninput_code -> C.input_code\nset_code -> C.set_code" ] diff --git a/docs/state-machines/key.dot b/docs/state-machines/key.dot index 01f5f2d..feda71b 100644 --- a/docs/state-machines/key.dot +++ b/docs/state-machines/key.dot @@ -55,7 +55,7 @@ digraph { S1 -> P1_compute [label="got_pake\npake good"] #S1 -> P_mood_lonely [label="close"] - P1_compute [label="compute_key\nM.add_message(version)\nB.got_key\nB.got_verifier\nR.got_key" shape="box"] + P1_compute [label="compute_key\nM.add_message(version)\nB.got_key\nR.got_key" shape="box"] P1_compute -> S4 S4 [label="S4: know_key" color="green"] diff --git a/docs/state-machines/machines.dot b/docs/state-machines/machines.dot index 39f7d83..eccc96d 100644 --- a/docs/state-machines/machines.dot +++ b/docs/state-machines/machines.dot @@ -42,7 +42,7 @@ digraph { #Boss -> Mailbox [color="blue"] Mailbox -> Order [style="dashed" label="got_message (once)"] - Key -> Boss [style="dashed" label="got_key\ngot_verifier\nscared"] + Key -> Boss [style="dashed" label="got_key\nscared"] Order -> Key [style="dashed" label="got_pake"] Order -> Receive [style="dashed" label="got_message"] #Boss -> Key [color="blue"] @@ -54,7 +54,7 @@ digraph { Key -> Receive [style="dashed" label="got_key"] Receive -> Boss [style="dashed" - label="happy\nscared\ngot_message"] + label="happy\nscared\ngot_verifier\ngot_message"] Nameplate -> Connection [style="dashed" label="tx_claim\ntx_release"] Connection -> Nameplate [style="dashed" diff --git a/docs/state-machines/receive.dot b/docs/state-machines/receive.dot index 3fe136d..ba757e1 100644 --- a/docs/state-machines/receive.dot +++ b/docs/state-machines/receive.dot @@ -19,7 +19,7 @@ digraph { S1 [label="S1:\nunverified key" color="orange"] S1 -> P_mood_scary [label="got_message\n(bad)"] S1 -> P1_accept_msg [label="got_message\n(good)" color="orange"] - P1_accept_msg [shape="box" label="S.got_verified_key\nB.happy\nB.got_message" + P1_accept_msg [shape="box" label="S.got_verified_key\nB.happy\nB.got_verifier\nB.got_message" color="orange"] P1_accept_msg -> S2 [color="orange"] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 4fa7213..2c93f0b 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -197,8 +197,8 @@ class Boss(object): @m.input() def got_code(self, code): pass - # Key sends (got_key, got_verifier, scared) - # Receive sends (got_message, happy, scared) + # Key sends (got_key, scared) + # Receive sends (got_message, happy, got_verifier, scared) @m.input() def happy(self): pass @m.input() @@ -307,11 +307,11 @@ class Boss(object): S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely]) S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send]) S1_lonely.upon(got_key, enter=S1_lonely, outputs=[W_got_key]) - S1_lonely.upon(got_verifier, enter=S1_lonely, outputs=[W_got_verifier]) S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_error]) S1_lonely.upon(error, enter=S4_closed, outputs=[W_close_with_error]) S2_happy.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome]) + S2_happy.upon(got_verifier, enter=S2_happy, outputs=[W_got_verifier]) S2_happy.upon(_got_phase, enter=S2_happy, outputs=[W_received]) S2_happy.upon(_got_version, enter=S2_happy, outputs=[process_version]) S2_happy.upon(scared, enter=S3_closing, outputs=[close_scared]) @@ -322,6 +322,7 @@ class Boss(object): S3_closing.upon(rx_unwelcome, enter=S3_closing, outputs=[]) S3_closing.upon(rx_error, enter=S3_closing, outputs=[]) + S3_closing.upon(got_verifier, enter=S3_closing, outputs=[]) S3_closing.upon(_got_phase, enter=S3_closing, outputs=[]) S3_closing.upon(_got_version, enter=S3_closing, outputs=[]) S3_closing.upon(happy, enter=S3_closing, outputs=[]) @@ -332,6 +333,7 @@ class Boss(object): S3_closing.upon(error, enter=S4_closed, outputs=[W_close_with_error]) S4_closed.upon(rx_unwelcome, enter=S4_closed, outputs=[]) + S4_closed.upon(got_verifier, enter=S4_closed, outputs=[]) S4_closed.upon(_got_phase, enter=S4_closed, outputs=[]) S4_closed.upon(_got_version, enter=S4_closed, outputs=[]) S4_closed.upon(happy, enter=S4_closed, outputs=[]) diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 622af65..91c972f 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -166,7 +166,6 @@ class _SortedKey(object): with self._timing.add("pake2", waiting="crypto"): key = self._sp.finish(msg2) self._B.got_key(key) - self._B.got_verifier(derive_key(key, b"wormhole:verifier")) phase = "version" data_key = derive_phase_key(key, self._side, phase) plaintext = dict_to_bytes(self._versions) diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index 1be5220..7003110 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -4,7 +4,7 @@ from attr import attrs, attrib from attr.validators import provides, instance_of from automat import MethodicalMachine from . import _interfaces -from ._key import derive_phase_key, decrypt_data, CryptoError +from ._key import derive_key, derive_phase_key, decrypt_data, CryptoError @attrs @implementer(_interfaces.IReceive) @@ -63,6 +63,9 @@ class Receive(object): def W_happy(self, phase, plaintext): self._B.happy() @m.output() + def W_got_verifier(self, phase, plaintext): + self._B.got_verifier(derive_key(self._key, b"wormhole:verifier")) + @m.output() def W_got_message(self, phase, plaintext): assert isinstance(phase, type("")), type(phase) assert isinstance(plaintext, type(b"")), type(plaintext) @@ -73,7 +76,8 @@ class Receive(object): S0_unknown_key.upon(got_key, enter=S1_unverified_key, outputs=[record_key]) S1_unverified_key.upon(got_message_good, enter=S2_verified_key, - outputs=[S_got_verified_key, W_happy, W_got_message]) + outputs=[S_got_verified_key, + W_happy, W_got_verifier, W_got_message]) S1_unverified_key.upon(got_message_bad, enter=S3_scared, outputs=[W_scared]) S2_verified_key.upon(got_message_bad, enter=S3_scared, diff --git a/src/wormhole/test/common.py b/src/wormhole/test/common.py index dd1d490..f0db4ac 100644 --- a/src/wormhole/test/common.py +++ b/src/wormhole/test/common.py @@ -91,3 +91,10 @@ def poll_until(predicate): d = defer.Deferred() reactor.callLater(0.001, d.callback, None) yield d + +@defer.inlineCallbacks +def pause_one_tick(): + # return a Deferred that won't fire until at least the next reactor tick + d = defer.Deferred() + reactor.callLater(0.001, d.callback, None) + yield d diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index df002b7..233524e 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -128,7 +128,8 @@ class Receive(unittest.TestCase): def build(self): events = [] r = _receive.Receive(u"side", timing.DebugTiming()) - b = Dummy("b", events, IBoss, "happy", "scared", "got_message") + b = Dummy("b", events, IBoss, + "happy", "scared", "got_verifier", "got_message") s = Dummy("s", events, ISend, "got_verified_key") r.wire(b, s) return r, b, s, events @@ -138,12 +139,14 @@ class Receive(unittest.TestCase): key = b"key" r.got_key(key) self.assertEqual(events, []) + verifier = derive_key(key, b"wormhole:verifier") phase1_key = derive_phase_key(key, u"side", u"phase1") data1 = b"data1" good_body = encrypt_data(phase1_key, data1) r.got_message(u"side", u"phase1", good_body) self.assertEqual(events, [("s.got_verified_key", key), ("b.happy",), + ("b.got_verifier", verifier), ("b.got_message", u"phase1", data1), ]) @@ -153,6 +156,7 @@ class Receive(unittest.TestCase): r.got_message(u"side", u"phase2", good_body) self.assertEqual(events, [("s.got_verified_key", key), ("b.happy",), + ("b.got_verifier", verifier), ("b.got_message", u"phase1", data1), ("b.got_message", u"phase2", data2), ]) @@ -181,12 +185,14 @@ class Receive(unittest.TestCase): key = b"key" r.got_key(key) self.assertEqual(events, []) + verifier = derive_key(key, b"wormhole:verifier") phase1_key = derive_phase_key(key, u"side", u"phase1") data1 = b"data1" good_body = encrypt_data(phase1_key, data1) r.got_message(u"side", u"phase1", good_body) self.assertEqual(events, [("s.got_verified_key", key), ("b.happy",), + ("b.got_verifier", verifier), ("b.got_message", u"phase1", data1), ]) @@ -196,6 +202,7 @@ class Receive(unittest.TestCase): r.got_message(u"side", u"phase2", bad_body) self.assertEqual(events, [("s.got_verified_key", key), ("b.happy",), + ("b.got_verifier", verifier), ("b.got_message", u"phase1", data1), ("b.scared",), ]) @@ -203,6 +210,7 @@ class Receive(unittest.TestCase): r.got_message(u"side", u"phase2", bad_body) self.assertEqual(events, [("s.got_verified_key", key), ("b.happy",), + ("b.got_verifier", verifier), ("b.got_message", u"phase1", data1), ("b.scared",), ]) @@ -216,7 +224,7 @@ class Key(unittest.TestCase): def build(self): events = [] k = _key.Key(u"appid", {}, u"side", timing.DebugTiming()) - b = Dummy("b", events, IBoss, "scared", "got_key", "got_verifier") + b = Dummy("b", events, IBoss, "scared", "got_key") m = Dummy("m", events, IMailbox, "add_message") r = Dummy("r", events, IReceive, "got_key") k.wire(b, m, r) @@ -237,11 +245,10 @@ class Key(unittest.TestCase): key2 = sp.finish(msg1_bytes) msg2 = dict_to_bytes({"pake_v1": bytes_to_hexstr(msg2_bytes)}) k.got_pake(msg2) - self.assertEqual(len(events), 4, events) + self.assertEqual(len(events), 3, events) self.assertEqual(events[0], ("b.got_key", key2)) - self.assertEqual(events[1][0], "b.got_verifier") - self.assertEqual(events[2][:2], ("m.add_message", "version")) - self.assertEqual(events[3], ("r.got_key", key2)) + self.assertEqual(events[1][:2], ("m.add_message", "version")) + self.assertEqual(events[2], ("r.got_key", key2)) def test_bad(self): k, b, m, r, events = self.build() @@ -274,16 +281,15 @@ class Key(unittest.TestCase): self.assertEqual(len(events), 0) k.got_code(code) - self.assertEqual(len(events), 5) + self.assertEqual(len(events), 4) self.assertEqual(events[0][:2], ("m.add_message", "pake")) msg1_json = events[0][2].decode("utf-8") msg1 = json.loads(msg1_json) msg1_bytes = hexstr_to_bytes(msg1["pake_v1"]) key2 = sp.finish(msg1_bytes) self.assertEqual(events[1], ("b.got_key", key2)) - self.assertEqual(events[2][0], "b.got_verifier") - self.assertEqual(events[3][:2], ("m.add_message", "version")) - self.assertEqual(events[4], ("r.got_key", key2)) + self.assertEqual(events[2][:2], ("m.add_message", "version")) + self.assertEqual(events[3], ("r.got_key", key2)) class Code(unittest.TestCase): def build(self): @@ -1178,8 +1184,8 @@ class Boss(unittest.TestCase): # pretend a peer message was correctly decrypted b.got_key(b"key") - b.got_verifier(b"verifier") b.happy() + b.got_verifier(b"verifier") b.got_message("version", b"{}") b.got_message("0", b"msg1") self.assertEqual(events, [("w.got_key", b"key"), diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index e43220a..824f23f 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -4,7 +4,7 @@ import mock from twisted.trial import unittest from twisted.internet import reactor from twisted.internet.defer import gatherResults, inlineCallbacks -from .common import ServerBase, poll_until +from .common import ServerBase, poll_until, pause_one_tick from .. import wormhole, _rendezvous from ..errors import (WrongPasswordError, KeyFormatError, WormholeClosed, LonelyError, @@ -115,10 +115,16 @@ class Wormholes(ServerBase, unittest.TestCase): code = yield w1.when_code() w2.set_code(code) + yield w1.when_key() + yield w2.when_key() + verifier1 = yield w1.when_verified() verifier2 = yield w2.when_verified() self.assertEqual(verifier1, verifier2) + self.successResultOf(w1.when_key()) + self.successResultOf(w2.when_key()) + version1 = yield w1.when_version() version2 = yield w2.when_version() # TODO: add the ability to set app-versions @@ -291,12 +297,13 @@ class Wormholes(ServerBase, unittest.TestCase): yield w1.close() yield w2.close() - # once closed, all Deferred-yielding API calls get an error - e = yield self.assertFailure(w1.when_code(), WormholeClosed) - self.assertEqual(e.args[0], "happy") - yield self.assertFailure(w1.when_verified(), WormholeClosed) - yield self.assertFailure(w1.when_version(), WormholeClosed) - yield self.assertFailure(w1.when_received(), WormholeClosed) + # once closed, all Deferred-yielding API calls get an immediate error + f = self.failureResultOf(w1.when_code(), WormholeClosed) + self.assertEqual(f.value.args[0], "happy") + self.failureResultOf(w1.when_key(), WormholeClosed) + self.failureResultOf(w1.when_verified(), WormholeClosed) + self.failureResultOf(w1.when_version(), WormholeClosed) + self.failureResultOf(w1.when_received(), WormholeClosed) @inlineCallbacks @@ -315,16 +322,64 @@ class Wormholes(ServerBase, unittest.TestCase): w1.send(b"should still work") w2.send(b"should still work") - # API calls that wait (i.e. get) will errback - yield self.assertFailure(w2.when_received(), WrongPasswordError) - yield self.assertFailure(w1.when_received(), WrongPasswordError) + key2 = yield w2.when_key() # should work + # w2 has just received w1.PAKE, and is about to send w2.VERSION + key1 = yield w1.when_key() # should work + # w1 has just received w2.PAKE, and is about to send w1.VERSION, and + # then will receive w2.VERSION. When it sees w2.VERSION, it will + # learn about the WrongPasswordError. + self.assertNotEqual(key1, key2) + # API calls that wait (i.e. get) will errback. We collect all these + # Deferreds early to exercise the wait-then-fail path + d1_verified = w1.when_verified() + d1_version = w1.when_version() + d1_received = w1.when_received() + d2_verified = w2.when_verified() + d2_version = w2.when_version() + d2_received = w2.when_received() + + # wait for each side to notice the failure yield self.assertFailure(w1.when_verified(), WrongPasswordError) - yield self.assertFailure(w1.when_version(), WrongPasswordError) + yield self.assertFailure(w2.when_verified(), WrongPasswordError) + # and then wait for the rest of the loops to fire. if we had+used + # eventual-send, this wouldn't be a problem + yield pause_one_tick() + + # now all the rest should have fired already + self.failureResultOf(d1_verified, WrongPasswordError) + self.failureResultOf(d1_version, WrongPasswordError) + self.failureResultOf(d1_received, WrongPasswordError) + self.failureResultOf(d2_verified, WrongPasswordError) + self.failureResultOf(d2_version, WrongPasswordError) + self.failureResultOf(d2_received, WrongPasswordError) + + # and at this point, with the failure safely noticed by both sides, + # new when_key() calls should signal the failure, even before we + # close + + # any new calls in the error state should immediately fail + self.failureResultOf(w1.when_key(), WrongPasswordError) + self.failureResultOf(w1.when_verified(), WrongPasswordError) + self.failureResultOf(w1.when_version(), WrongPasswordError) + self.failureResultOf(w1.when_received(), WrongPasswordError) + self.failureResultOf(w2.when_key(), WrongPasswordError) + self.failureResultOf(w2.when_verified(), WrongPasswordError) + self.failureResultOf(w2.when_version(), WrongPasswordError) + self.failureResultOf(w2.when_received(), WrongPasswordError) yield self.assertFailure(w1.close(), WrongPasswordError) yield self.assertFailure(w2.close(), WrongPasswordError) + # API calls should still get the error, not WormholeClosed + self.failureResultOf(w1.when_key(), WrongPasswordError) + self.failureResultOf(w1.when_verified(), WrongPasswordError) + self.failureResultOf(w1.when_version(), WrongPasswordError) + self.failureResultOf(w1.when_received(), WrongPasswordError) + self.failureResultOf(w2.when_key(), WrongPasswordError) + self.failureResultOf(w2.when_verified(), WrongPasswordError) + self.failureResultOf(w2.when_version(), WrongPasswordError) + self.failureResultOf(w2.when_received(), WrongPasswordError) @inlineCallbacks def test_wrong_password_with_spaces(self): diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index c8ca8b0..0077020 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -97,6 +97,7 @@ class _DelegatedWormhole(object): def got_code(self, code): self._delegate.wormhole_code(code) def got_key(self, key): + self._delegate.wormhole_key() self._key = key # for derive_key() def got_verifier(self, verifier): self._delegate.wormhole_verified(verifier) @@ -113,6 +114,7 @@ class _DeferredWormhole(object): self._code = None self._code_observers = [] self._key = None + self._key_observers = [] self._verifier = None self._verifier_observers = [] self._versions = None @@ -138,6 +140,15 @@ class _DeferredWormhole(object): self._code_observers.append(d) return d + def when_key(self): + if self._observer_result is not None: + return defer.fail(self._observer_result) + if self._key is not None: + return defer.succeed(self._key) + d = defer.Deferred() + self._key_observers.append(d) + return d + def when_verified(self): if self._observer_result is not None: return defer.fail(self._observer_result) @@ -180,7 +191,7 @@ class _DeferredWormhole(object): """Derive a new key from the established wormhole channel for some other purpose. This is a deterministic randomized function of the session key and the 'purpose' string (unicode/py3-string). This - cannot be called until when_verifier() has fired, nor after close() + cannot be called until when_verified() has fired, nor after close() was called. """ if not isinstance(purpose, type("")): raise TypeError(type(purpose)) @@ -210,6 +221,9 @@ class _DeferredWormhole(object): self._code_observers[:] = [] def got_key(self, key): self._key = key # for derive_key() + for d in self._key_observers: + d.callback(key) + self._key_observers[:] = [] def got_verifier(self, verifier): self._verifier = verifier for d in self._verifier_observers: @@ -232,10 +246,12 @@ class _DeferredWormhole(object): if isinstance(result, Exception): self._observer_result = self._closed_result = failure.Failure(result) else: - # pending w.verify()/w.version()/w.read() get an error + # pending w.key()/w.verify()/w.version()/w.read() get an error self._observer_result = WormholeClosed(result) # but w.close() only gets error if we're unhappy self._closed_result = result + for d in self._key_observers: + d.errback(self._observer_result) for d in self._verifier_observers: d.errback(self._observer_result) for d in self._version_observers: