add w.when_key(), fix w.when_verified() to fire later

Previously, w.when_verified() was documented to fire only after a valid
encrypted message was received, but in fact it fired as soon as the shared
key was derived (before any encrypted messages are seen, so no actual
"verification" could occur yet).

This fixes that, and also adds a new w.when_key() API call which fires at the
earlier point. Having something which fires early is useful for the CLI
commands that want to print a pacifier message when the peer is responding
slowly. In particular it helps detect the case where 'wormhole send' has quit
early (after depositing the PAKE message on the server, but before the
receiver has started). In this case, the receiver will compute the shared
key, but then wait forever hoping for a VERSION that will never come. By
starting a timer when w.when_key() fires, and cancelling it when
w.when_verified() fires, we have a good place to tell the user that something
is taking longer than it should have.

This shifts responsibility for notifying Boss.got_verifier, out of Key and
into Receive, since Receive is what notices the first valid encrypted
message. It also shifts the Boss's ordering expectations: it now receives
B.happy() before B.got_verifier(), and consequently got_verifier ought to
arrive in the S2_happy state rather than S1_lonely.
This commit is contained in:
Brian Warner 2017-04-06 18:27:41 -07:00
parent 67d53f1388
commit 83e55f1f3e
12 changed files with 143 additions and 40 deletions

View File

@ -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

View File

@ -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"
]

View File

@ -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"]

View File

@ -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"

View File

@ -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"]

View File

@ -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=[])

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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"),

View File

@ -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):

View File

@ -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: