API updates, make most tests pass, disable others

* finally wire up "application versions"
* remove when_verifier (which used to fire after key establishment, but
  before the VERSION message was received or verified)
* fire when_verified and when_version at the same time (after VERSION is
  verified), but with different args
This commit is contained in:
Brian Warner 2017-03-07 12:34:36 +01:00
parent e518f2b799
commit 5f9894ca63
6 changed files with 164 additions and 129 deletions

View File

@ -274,24 +274,25 @@ functions on the delegate object. In Deferred mode, the application retrieves
Deferred objects from the wormhole, and event dispatch is performed by firing
those Deferreds.
* got_code (`yield w.when_code()` / `dg.wormhole_got_code(code)`): fired when the
* got_code (`yield w.when_code()` / `dg.wormhole_code(code)`): fired when the
wormhole code is established, either after `w.generate_code()` finishes the
generation process, or when the Input Helper returned by `w.type_code()`
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.
* got_verifier (`yield w.when_verifier()` / `dg.wormhole_got_verifier(verf)`:
fired when the key-exchange process has completed, and this side has
learned the shared key. The "verifier" is a byte string with a hash of the
shared session key; clients can compare them (probably as hex) to ensure
that they're really talking to each other, and not to a man-in-the-middle.
When `got_verifier` happens, this side has not yet seen evidence that the
peer has used the correct wormhole code.
* got_version (`yield w.when_version()` / `dg.wormhole_got_version(version)`:
fired when the VERSION message arrives from the peer. This serves two
purposes. The first is that it provide confirmation that the peer (or a
man-in-the-middle) has used the correct wormhole code. The second is
delivery of the "app_versions" data (passed into `wormhole.create`).
* 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
string with a hash of the shared session key; clients can compare them
(probably as hex) to ensure that they're really talking to each other, and
not to a man-in-the-middle. When `got_verifier` happens, this side knows
that *someone* has used the correct wormhole code; if someone used the
wrong code, the VERSION message cannot be decrypted, and the wormhole will
be closed instead.
* version (`yield w.when_version()` / `dg.wormhole_version(version)`:
fired when the VERSION message arrives from the peer. This fires at the
same time as `verified`, but delivers the "app_versions" data (passed into
`wormhole.create`) instead of the verifier string.
* received (`yield w.when_received()` / `dg.wormhole_received(data)`: fired
each time a data message arrives from the peer, with the bytestring that
the peer passed into `w.send(data)`.
@ -391,19 +392,19 @@ in python3):
## Full API list
action | Deferred-Mode | Delegated-Mode
-------------------------- | --------------------- | ----------------------------
w.generate_code(length=2) | |
w.set_code(code) | |
h=w.type_code() | |
| d=w.when_code() | dg.wormhole_got_code(code)
| d=w.when_verifier() | dg.wormhole_got_verifier(verf)
| d=w.when_version() | dg.wormhole_got_version(version)
w.send(data) | |
| d=w.when_received() | dg.wormhole_received(data)
key=w.derive_key(purpose, length) | |
w.close() | | dg.wormhole_closed(result)
| d=w.close() |
action | Deferred-Mode | Delegated-Mode
-------------------------- | -------------------- | --------------
w.generate_code(length=2) | |
w.set_code(code) | |
h=w.type_code() | |
| d=w.when_code() | dg.wormhole_code(code)
| d=w.when_verified() | dg.wormhole_verified(verifier)
| d=w.when_version() | dg.wormhole_version(version)
w.send(data) | |
| d=w.when_received() | dg.wormhole_received(data)
key=w.derive_key(purpose, length) | |
w.close() | | dg.wormhole_closed(result)
| d=w.close() |
## Dilation

View File

@ -27,6 +27,7 @@ class Boss(object):
_side = attrib(validator=instance_of(type(u"")))
_url = attrib(validator=instance_of(type(u"")))
_appid = attrib(validator=instance_of(type(u"")))
_versions = attrib(validator=instance_of(dict))
_welcome_handler = attrib() # TODO: validator: callable
_reactor = attrib()
_journal = attrib(validator=provides(_interfaces.IJournal))
@ -41,7 +42,7 @@ class Boss(object):
self._M = Mailbox(self._side)
self._S = Send(self._side, self._timing)
self._O = Order(self._side, self._timing)
self._K = Key(self._appid, self._side, self._timing)
self._K = Key(self._appid, self._versions, self._side, self._timing)
self._R = Receive(self._side, self._timing)
self._RC = RendezvousConnector(self._url, self._appid, self._side,
self._reactor, self._journal,
@ -188,7 +189,7 @@ class Boss(object):
self._their_versions = bytes_to_dict(plaintext)
# but this part is app-to-app
app_versions = self._their_versions.get("app_versions", {})
self._W.got_version(app_versions)
self._W.got_versions(app_versions)
@m.output()
def S_send(self, plaintext):

View File

@ -56,6 +56,7 @@ def encrypt_data(key, plaintext):
@implementer(_interfaces.IKey)
class Key(object):
_appid = attrib(validator=instance_of(type(u"")))
_versions = attrib(validator=instance_of(dict))
_side = attrib(validator=instance_of(type(u"")))
_timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine()
@ -114,8 +115,7 @@ class Key(object):
self._B.got_verifier(derive_key(key, b"wormhole:verifier"))
phase = "version"
data_key = derive_phase_key(key, self._side, phase)
my_versions = {} # TODO: get from Wormhole?
plaintext = dict_to_bytes(my_versions)
plaintext = dict_to_bytes(self._versions)
encrypted = encrypt_data(data_key, plaintext)
self._M.add_message(phase, encrypted)
self._R.got_key(key)

View File

@ -6,9 +6,9 @@ from twisted.trial import unittest
from twisted.internet import reactor
from twisted.internet.defer import Deferred, gatherResults, inlineCallbacks
from .common import ServerBase
from .. import wormhole, _order
from .. import wormhole, _rendezvous
from ..errors import (WrongPasswordError, WelcomeError, InternalError,
KeyFormatError)
KeyFormatError, WormholeClosed, LonelyError)
from spake2 import SPAKE2_Symmetric
from ..timing import DebugTiming
from ..util import (bytes_to_dict, dict_to_bytes,
@ -116,7 +116,7 @@ class InputCode(unittest.TestCase):
res = self.successResultOf(d)
self.assertEqual(res, ["123"])
self.assertEqual(stderr.getvalue(), "")
InputCode.skip = "not yet"
class GetCode(unittest.TestCase):
def test_get(self):
@ -134,6 +134,7 @@ class GetCode(unittest.TestCase):
pieces = code.split("-")
self.assertEqual(len(pieces), 3) # nameplate plus two words
self.assert_(re.search(r'^\d+-\w+-\w+$', code), code)
GetCode.skip = "not yet"
class Basic(unittest.TestCase):
def tearDown(self):
@ -714,7 +715,7 @@ class Basic(unittest.TestCase):
w.derive_key, "foo", SecretBox.KEY_SIZE)
self.failureResultOf(w.get(), WrongPasswordError)
self.failureResultOf(w.verify(), WrongPasswordError)
Basic.skip = "being replaced by test_wormhole_new"
# event orderings to exercise:
#
@ -735,14 +736,15 @@ class Wormholes(ServerBase, unittest.TestCase):
@inlineCallbacks
def test_basic(self):
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
code = yield w1.get_code()
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
w2.set_code(code)
w1.send(b"data1")
w2.send(b"data2")
dataX = yield w1.get()
dataY = yield w2.get()
dataX = yield w1.when_received()
dataY = yield w2.when_received()
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
yield w1.close()
@ -753,14 +755,15 @@ class Wormholes(ServerBase, unittest.TestCase):
# the two sides use random nonces for their messages, so it's ok for
# both to try and send the same body: they'll result in distinct
# encrypted messages
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
code = yield w1.get_code()
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
w2.set_code(code)
w1.send(b"data")
w2.send(b"data")
dataX = yield w1.get()
dataY = yield w2.get()
dataX = yield w1.when_received()
dataY = yield w2.when_received()
self.assertEqual(dataX, b"data")
self.assertEqual(dataY, b"data")
yield w1.close()
@ -768,14 +771,15 @@ class Wormholes(ServerBase, unittest.TestCase):
@inlineCallbacks
def test_interleaved(self):
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
code = yield w1.get_code()
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
w2.set_code(code)
w1.send(b"data1")
dataY = yield w2.get()
dataY = yield w2.when_received()
self.assertEqual(dataY, b"data1")
d = w1.get()
d = w1.when_received()
w2.send(b"data2")
dataX = yield d
self.assertEqual(dataX, b"data2")
@ -784,22 +788,23 @@ class Wormholes(ServerBase, unittest.TestCase):
@inlineCallbacks
def test_unidirectional(self):
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
code = yield w1.get_code()
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
w2.set_code(code)
w1.send(b"data1")
dataY = yield w2.get()
dataY = yield w2.when_received()
self.assertEqual(dataY, b"data1")
yield w1.close()
yield w2.close()
@inlineCallbacks
def test_early(self):
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
w1 = wormhole.create(APPID, self.relayurl, reactor)
w1.send(b"data1")
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
d = w2.get()
w2 = wormhole.create(APPID, self.relayurl, reactor)
d = w2.when_received()
w1.set_code("123-abc-def")
w2.set_code("123-abc-def")
dataY = yield d
@ -809,12 +814,12 @@ class Wormholes(ServerBase, unittest.TestCase):
@inlineCallbacks
def test_fixed_code(self):
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.set_code("123-purple-elephant")
w2.set_code("123-purple-elephant")
w1.send(b"data1"), w2.send(b"data2")
dl = yield self.doBoth(w1.get(), w2.get())
dl = yield self.doBoth(w1.when_received(), w2.when_received())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
@ -824,28 +829,52 @@ class Wormholes(ServerBase, unittest.TestCase):
@inlineCallbacks
def test_multiple_messages(self):
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.set_code("123-purple-elephant")
w2.set_code("123-purple-elephant")
w1.send(b"data1"), w2.send(b"data2")
w1.send(b"data3"), w2.send(b"data4")
dl = yield self.doBoth(w1.get(), w2.get())
dl = yield self.doBoth(w1.when_received(), w2.when_received())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
dl = yield self.doBoth(w1.get(), w2.get())
dl = yield self.doBoth(w1.when_received(), w2.when_received())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data4")
self.assertEqual(dataY, b"data3")
yield w1.close()
yield w2.close()
@inlineCallbacks
def test_closed(self):
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.set_code("123-foo")
w2.set_code("123-foo")
# let it connect and become HAPPY
yield w1.when_version()
yield w2.when_version()
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)
@inlineCallbacks
def test_wrong_password(self):
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
code = yield w1.get_code()
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
w2.set_code(code+"not")
# That's enough to allow both sides to discover the mismatch, but
# only after the confirmation message gets through. API calls that
@ -855,44 +884,41 @@ class Wormholes(ServerBase, unittest.TestCase):
w2.send(b"should still work")
# API calls that wait (i.e. get) will errback
yield self.assertFailure(w2.get(), WrongPasswordError)
yield self.assertFailure(w1.get(), WrongPasswordError)
yield self.assertFailure(w2.when_received(), WrongPasswordError)
yield self.assertFailure(w1.when_received(), WrongPasswordError)
yield self.assertFailure(w1.when_verified(), WrongPasswordError)
yield self.assertFailure(w1.when_version(), WrongPasswordError)
yield self.assertFailure(w1.close(), WrongPasswordError)
yield self.assertFailure(w2.close(), WrongPasswordError)
yield w1.close()
yield w2.close()
self.flushLoggedErrors(WrongPasswordError)
@inlineCallbacks
def test_wrong_password_with_spaces(self):
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
code = yield w1.get_code()
code_no_dashes = code.replace('-', ' ')
w = wormhole.create(APPID, self.relayurl, reactor)
badcode = "4 oops spaces"
with self.assertRaises(KeyFormatError) as ex:
w2.set_code(code_no_dashes)
expected_msg = "code (%s) contains spaces." % (code_no_dashes,)
w.set_code(badcode)
expected_msg = "code (%s) contains spaces." % (badcode,)
self.assertEqual(expected_msg, str(ex.exception))
yield w1.close()
yield w2.close()
self.flushLoggedErrors(KeyFormatError)
yield self.assertFailure(w.close(), LonelyError)
@inlineCallbacks
def test_verifier(self):
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
code = yield w1.get_code()
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
w2.set_code(code)
v1 = yield w1.verify()
v2 = yield w2.verify()
v1 = yield w1.when_verified()
v2 = yield w2.when_verified()
self.failUnlessEqual(type(v1), type(b""))
self.failUnlessEqual(v1, v2)
w1.send(b"data1")
w2.send(b"data2")
dataX = yield w1.get()
dataY = yield w2.get()
dataX = yield w1.when_received()
dataY = yield w2.when_received()
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
yield w1.close()
@ -901,16 +927,17 @@ class Wormholes(ServerBase, unittest.TestCase):
@inlineCallbacks
def test_versions(self):
# there's no API for this yet, but make sure the internals work
w1 = wormhole.wormhole(APPID, self.relayurl, reactor)
w1._my_versions = {"w1": 123}
w2 = wormhole.wormhole(APPID, self.relayurl, reactor)
w2._my_versions = {"w2": 456}
code = yield w1.get_code()
w1 = wormhole.create(APPID, self.relayurl, reactor,
versions={"w1": 123})
w2 = wormhole.create(APPID, self.relayurl, reactor,
versions={"w2": 456})
w1.allocate_code()
code = yield w1.when_code()
w2.set_code(code)
yield w1.verify()
self.assertEqual(w1._their_versions, {"w2": 456})
yield w2.verify()
self.assertEqual(w2._their_versions, {"w1": 123})
w1_versions = yield w2.when_version()
self.assertEqual(w1_versions, {"w1": 123})
w2_versions = yield w1.when_version()
self.assertEqual(w2_versions, {"w2": 456})
yield w1.close()
yield w2.close()
@ -923,50 +950,52 @@ class Wormholes(ServerBase, unittest.TestCase):
# incoming PAKE message was received, which would cause
# SPAKE2.finish() to be called a second time, which throws an error
# (which, being somewhat unexpected, caused a hang rather than a
# clear exception).
with mock.patch("wormhole.wormhole._order", MessageDoubler):
# clear exception). The Mailbox object is responsible for
# deduplication, so we must patch the RendezvousConnector to simulate
# duplicated messages.
with mock.patch("wormhole._boss.RendezvousConnector", MessageDoubler):
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.set_code("123-purple-elephant")
w2.set_code("123-purple-elephant")
w1.send(b"data1"), w2.send(b"data2")
dl = yield self.doBoth(w1.get(), w2.get())
dl = yield self.doBoth(w1.when_received(), w2.when_received())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
yield w1.close()
yield w2.close()
class MessageDoubler(_order.Order):
class MessageDoubler(_rendezvous.RendezvousConnector):
# we could double messages on the sending side, but a future server will
# strip those duplicates, so to really exercise the receiver, we must
# double them on the inbound side instead
#def _msg_send(self, phase, body):
# wormhole._Wormhole._msg_send(self, phase, body)
# self._ws_send_command("add", phase=phase, body=bytes_to_hexstr(body))
def got_message(self, side, phase, body):
_order.Order.got_message(self, side, phase, body)
_order.Order.got_message(self, side, phase, body)
def _response_handle_message(self, msg):
_rendezvous.RendezvousConnector._response_handle_message(self, msg)
_rendezvous.RendezvousConnector._response_handle_message(self, msg)
class Errors(ServerBase, unittest.TestCase):
@inlineCallbacks
def test_codes_1(self):
w = wormhole.wormhole(APPID, self.relayurl, reactor)
w = wormhole.create(APPID, self.relayurl, reactor)
# definitely too early
self.assertRaises(InternalError, w.derive_key, "purpose", 12)
w.set_code("123-purple-elephant")
# code can only be set once
self.assertRaises(InternalError, w.set_code, "123-nope")
yield self.assertFailure(w.get_code(), InternalError)
yield self.assertFailure(w.when_code(), InternalError)
yield self.assertFailure(w.input_code(), InternalError)
yield w.close()
@inlineCallbacks
def test_codes_2(self):
w = wormhole.wormhole(APPID, self.relayurl, reactor)
yield w.get_code()
w = wormhole.create(APPID, self.relayurl, reactor)
yield w.when_code()
self.assertRaises(InternalError, w.set_code, "123-nope")
yield self.assertFailure(w.get_code(), InternalError)
yield self.assertFailure(w.when_code(), InternalError)
yield self.assertFailure(w.input_code(), InternalError)
yield w.close()

View File

@ -57,8 +57,8 @@ class New(ServerBase, unittest.TestCase):
code2 = yield w2.when_code()
self.assertEqual(code, code2)
verifier1 = yield w1.when_verifier()
verifier2 = yield w2.when_verifier()
verifier1 = yield w1.when_verified()
verifier2 = yield w2.when_verified()
self.assertEqual(verifier1, verifier2)
version1 = yield w1.when_version()
@ -88,7 +88,7 @@ class New(ServerBase, unittest.TestCase):
w1.allocate_code(2)
code = yield w1.when_code()
w2 = wormhole.create(APPID, self.relayurl, reactor)
w2.set_code(code+", NOT")
w2.set_code(code+"NOT")
code2 = yield w2.when_code()
self.assertNotEqual(code, code2)

View File

@ -25,7 +25,7 @@ from .util import to_bytes
# w.send(data)
# app.wormhole_got_code(code)
# app.wormhole_got_verifier(verifier)
# app.wormhole_got_version(version)
# app.wormhole_got_version(versions)
# app.wormhole_receive(data)
# w.close()
# app.wormhole_closed()
@ -117,15 +117,15 @@ class _DelegatedWormhole(object):
# from below
def got_code(self, code):
self._delegate.wormhole_got_code(code)
self._delegate.wormhole_code(code)
def got_welcome(self, welcome):
pass # TODO
def got_key(self, key):
self._key = key # for derive_key()
def got_verifier(self, verifier):
self._delegate.wormhole_got_verifier(verifier)
def got_version(self, version):
self._delegate.wormhole_got_version(version)
self._delegate.wormhole_verified(verifier)
def got_versions(self, versions):
self._delegate.wormhole_version(versions)
def received(self, plaintext):
self._delegate.wormhole_received(plaintext)
def closed(self, result):
@ -139,7 +139,7 @@ class _DeferredWormhole(object):
self._key = None
self._verifier = None
self._verifier_observers = []
self._version = None
self._versions = None
self._version_observers = []
self._received_data = []
self._received_observers = []
@ -162,7 +162,7 @@ class _DeferredWormhole(object):
self._code_observers.append(d)
return d
def when_verifier(self):
def when_verified(self):
if self._observer_result is not None:
return defer.fail(self._observer_result)
if self._verifier is not None:
@ -174,8 +174,8 @@ class _DeferredWormhole(object):
def when_version(self):
if self._observer_result is not None:
return defer.fail(self._observer_result)
if self._version is not None:
return defer.succeed(self._version)
if self._versions is not None:
return defer.succeed(self._versions)
d = defer.Deferred()
self._version_observers.append(d)
return d
@ -241,10 +241,10 @@ class _DeferredWormhole(object):
for d in self._verifier_observers:
d.callback(verifier)
self._verifier_observers[:] = []
def got_version(self, version):
self._version = version
def got_versions(self, versions):
self._versions = versions
for d in self._version_observers:
d.callback(version)
d.callback(versions)
self._version_observers[:] = []
def received(self, plaintext):
@ -272,8 +272,9 @@ class _DeferredWormhole(object):
d.callback(self._closed_result)
def create(appid, relay_url, reactor, delegate=None, journal=None,
tor_manager=None, timing=None, welcome_handler=None,
def create(appid, relay_url, reactor, versions={},
delegate=None, journal=None, tor_manager=None,
timing=None, welcome_handler=None,
stderr=sys.stderr):
timing = timing or DebugTiming()
side = bytes_to_hexstr(os.urandom(5))
@ -287,7 +288,10 @@ def create(appid, relay_url, reactor, delegate=None, journal=None,
w = _DelegatedWormhole(delegate)
else:
w = _DeferredWormhole()
b = Boss(w, side, relay_url, appid, welcome_handler, reactor, journal,
wormhole_versions = {} # will be used to indicate Wormhole capabilities
wormhole_versions["app_versions"] = versions # app-specific capabilities
b = Boss(w, side, relay_url, appid, wormhole_versions,
welcome_handler, reactor, journal,
tor_manager, timing)
w._set_boss(b)
b.start()