|
@@ -34,7 +34,7 @@ from Strobe.Strobe import Strobe, KeccakF |
|
|
from Strobe.Strobe import AuthenticationFailed |
|
|
from Strobe.Strobe import AuthenticationFailed |
|
|
|
|
|
|
|
|
import lora_comms |
|
|
import lora_comms |
|
|
from lora_comms import make_pktbuf |
|
|
|
|
|
|
|
|
from lora_comms import make_pktbuf, X25519 |
|
|
import multicast |
|
|
import multicast |
|
|
from util import * |
|
|
from util import * |
|
|
|
|
|
|
|
@@ -51,22 +51,37 @@ CMD_ADV = 6 # arg: ([cnt]): advances to the next cnt (default 1) command |
|
|
CMD_CLEAR = 7 # arg: (): clears all future commands, but keeps current running |
|
|
CMD_CLEAR = 7 # arg: (): clears all future commands, but keeps current running |
|
|
|
|
|
|
|
|
class LORANode(object): |
|
|
class LORANode(object): |
|
|
'''Implement a LORANode initiator.''' |
|
|
|
|
|
|
|
|
'''Implement a LORANode initiator. |
|
|
|
|
|
|
|
|
|
|
|
There are currently two implemented modes, one is shared, and then |
|
|
|
|
|
a shared key must be provided to the shared keyword argument. |
|
|
|
|
|
|
|
|
|
|
|
The other is ecdhe mode, which requires an X25519 key to be passed |
|
|
|
|
|
in to init_key, and the respondent's public key to be passed in to |
|
|
|
|
|
resp_pub. |
|
|
|
|
|
''' |
|
|
|
|
|
|
|
|
SHARED_DOMAIN = b'com.funkthat.lora.irrigation.shared.v0.0.1' |
|
|
SHARED_DOMAIN = b'com.funkthat.lora.irrigation.shared.v0.0.1' |
|
|
ECDHE_DOMAIN = b'com.funkthat.lora.irrigation.ecdhe.v0.0.1' |
|
|
ECDHE_DOMAIN = b'com.funkthat.lora.irrigation.ecdhe.v0.0.1' |
|
|
|
|
|
|
|
|
MAC_LEN = 8 |
|
|
MAC_LEN = 8 |
|
|
|
|
|
|
|
|
def __init__(self, syncdatagram, shared=None, ecdhe_key=None, resp_pub=None): |
|
|
|
|
|
|
|
|
def __init__(self, syncdatagram, shared=None, init_key=None, resp_pub=None): |
|
|
self.sd = syncdatagram |
|
|
self.sd = syncdatagram |
|
|
self.st = Strobe(self.SHARED_DOMAIN, F=KeccakF(800)) |
|
|
|
|
|
if shared is not None: |
|
|
if shared is not None: |
|
|
|
|
|
self.st = Strobe(self.SHARED_DOMAIN, F=KeccakF(800)) |
|
|
self.st.key(shared) |
|
|
self.st.key(shared) |
|
|
|
|
|
self.start = self.shared_start |
|
|
|
|
|
elif init_key is not None and resp_pub is not None: |
|
|
|
|
|
self.st = Strobe(self.ECDHE_DOMAIN, F=KeccakF(800)) |
|
|
|
|
|
self.key = init_key |
|
|
|
|
|
self.resp_pub = resp_pub |
|
|
|
|
|
self.st.key(init_key.getpub() + resp_pub) |
|
|
|
|
|
self.start = self.ecdhe_start |
|
|
else: |
|
|
else: |
|
|
raise RuntimeError |
|
|
|
|
|
|
|
|
raise RuntimeError('invalid combination of keys provided') |
|
|
|
|
|
|
|
|
async def start(self): |
|
|
|
|
|
|
|
|
async def shared_start(self): |
|
|
resp = await self.sendrecvvalid(os.urandom(16) + b'reqreset') |
|
|
resp = await self.sendrecvvalid(os.urandom(16) + b'reqreset') |
|
|
|
|
|
|
|
|
self.st.ratchet() |
|
|
self.st.ratchet() |
|
@@ -77,9 +92,26 @@ class LORANode(object): |
|
|
raise RuntimeError('got invalid response: %s' % |
|
|
raise RuntimeError('got invalid response: %s' % |
|
|
repr(pkt)) |
|
|
repr(pkt)) |
|
|
|
|
|
|
|
|
async def sendrecvvalid(self, msg): |
|
|
|
|
|
|
|
|
async def ecdhe_start(self): |
|
|
|
|
|
ephkey = X25519.gen() |
|
|
|
|
|
|
|
|
|
|
|
resp = await self.sendrecvvalid(ephkey.getpub() + b'reqreset', |
|
|
|
|
|
fun=lambda: self.st.key(ephkey.dh(self.resp_pub) + self.key.dh(self.resp_pub))) |
|
|
|
|
|
|
|
|
|
|
|
self.st.key(ephkey.dh(resp) + self.key.dh(resp)) |
|
|
|
|
|
|
|
|
|
|
|
pkt = await self.sendrecvvalid(b'confirm') |
|
|
|
|
|
|
|
|
|
|
|
if pkt != b'confirmed': |
|
|
|
|
|
raise RuntimeError('got invalid response: %s' % |
|
|
|
|
|
repr(pkt)) |
|
|
|
|
|
|
|
|
|
|
|
async def sendrecvvalid(self, msg, fun=None): |
|
|
msg = self.st.send_enc(msg) + self.st.send_mac(self.MAC_LEN) |
|
|
msg = self.st.send_enc(msg) + self.st.send_mac(self.MAC_LEN) |
|
|
|
|
|
|
|
|
|
|
|
if fun is not None: |
|
|
|
|
|
fun() |
|
|
|
|
|
|
|
|
origstate = self.st.copy() |
|
|
origstate = self.st.copy() |
|
|
|
|
|
|
|
|
while True: |
|
|
while True: |
|
@@ -605,12 +637,154 @@ class TestSequencing(unittest.IsolatedAsyncioTestCase): |
|
|
|
|
|
|
|
|
class TestLORANode(unittest.IsolatedAsyncioTestCase): |
|
|
class TestLORANode(unittest.IsolatedAsyncioTestCase): |
|
|
shared_domain = b'com.funkthat.lora.irrigation.shared.v0.0.1' |
|
|
shared_domain = b'com.funkthat.lora.irrigation.shared.v0.0.1' |
|
|
|
|
|
ecdhe_domain = b'com.funkthat.lora.irrigation.ecdhe.v0.0.1' |
|
|
|
|
|
|
|
|
def test_initparams(self): |
|
|
def test_initparams(self): |
|
|
# make sure no keys fails |
|
|
# make sure no keys fails |
|
|
with self.assertRaises(RuntimeError): |
|
|
with self.assertRaises(RuntimeError): |
|
|
l = LORANode(None) |
|
|
l = LORANode(None) |
|
|
|
|
|
|
|
|
|
|
|
@timeout(2) |
|
|
|
|
|
async def test_lora_ecdhe(self): |
|
|
|
|
|
_self = self |
|
|
|
|
|
initkey = X25519.gen() |
|
|
|
|
|
respkey = X25519.gen() |
|
|
|
|
|
|
|
|
|
|
|
class TestSD(MockSyncDatagram): |
|
|
|
|
|
async def sendgettest(self, msg): |
|
|
|
|
|
'''Send the message, but make sure that if a |
|
|
|
|
|
bad message is sent afterward, that it replies |
|
|
|
|
|
w/ the same previous message. |
|
|
|
|
|
''' |
|
|
|
|
|
|
|
|
|
|
|
await self.put(msg) |
|
|
|
|
|
resp = await self.get() |
|
|
|
|
|
|
|
|
|
|
|
await self.put(b'bogusmsg' * 5) |
|
|
|
|
|
|
|
|
|
|
|
resp2 = await self.get() |
|
|
|
|
|
|
|
|
|
|
|
_self.assertEqual(resp, resp2) |
|
|
|
|
|
|
|
|
|
|
|
return resp |
|
|
|
|
|
|
|
|
|
|
|
async def runner(self): |
|
|
|
|
|
# as respondant |
|
|
|
|
|
|
|
|
|
|
|
l = Strobe(_self.ecdhe_domain, F=KeccakF(800)) |
|
|
|
|
|
|
|
|
|
|
|
l.key(initkey.getpub() + respkey.getpub()) |
|
|
|
|
|
|
|
|
|
|
|
# start handshake |
|
|
|
|
|
r = await self.get() |
|
|
|
|
|
|
|
|
|
|
|
# get eph key w/ reqreset |
|
|
|
|
|
pkt = l.recv_enc(r[:-8]) |
|
|
|
|
|
l.recv_mac(r[-8:]) |
|
|
|
|
|
|
|
|
|
|
|
assert pkt.endswith(b'reqreset') |
|
|
|
|
|
|
|
|
|
|
|
ephpub = pkt[:-len(b'reqreset')] |
|
|
|
|
|
|
|
|
|
|
|
# make sure junk gets ignored |
|
|
|
|
|
await self.put(b'sdlfkj') |
|
|
|
|
|
|
|
|
|
|
|
# and that the packet remains the same |
|
|
|
|
|
_self.assertEqual(r, await self.get()) |
|
|
|
|
|
|
|
|
|
|
|
# and a couple more times |
|
|
|
|
|
await self.put(b'0' * 24) |
|
|
|
|
|
_self.assertEqual(r, await self.get()) |
|
|
|
|
|
await self.put(b'0' * 32) |
|
|
|
|
|
_self.assertEqual(r, await self.get()) |
|
|
|
|
|
|
|
|
|
|
|
# update the keys |
|
|
|
|
|
l.key(respkey.dh(ephpub) + respkey.dh(initkey.getpub())) |
|
|
|
|
|
|
|
|
|
|
|
# generate our eph key |
|
|
|
|
|
ephkey = X25519.gen() |
|
|
|
|
|
|
|
|
|
|
|
# send the response |
|
|
|
|
|
await self.put(l.send_enc(ephkey.getpub()) + |
|
|
|
|
|
l.send_mac(8)) |
|
|
|
|
|
|
|
|
|
|
|
l.key(ephkey.dh(ephpub) + ephkey.dh(initkey.getpub())) |
|
|
|
|
|
|
|
|
|
|
|
# get the confirmation message |
|
|
|
|
|
r = await self.get() |
|
|
|
|
|
|
|
|
|
|
|
# test the resend capabilities |
|
|
|
|
|
await self.put(b'0' * 24) |
|
|
|
|
|
_self.assertEqual(r, await self.get()) |
|
|
|
|
|
|
|
|
|
|
|
# decode confirmation message |
|
|
|
|
|
c = l.recv_enc(r[:-8]) |
|
|
|
|
|
l.recv_mac(r[-8:]) |
|
|
|
|
|
|
|
|
|
|
|
# assert that we got it |
|
|
|
|
|
_self.assertEqual(c, b'confirm') |
|
|
|
|
|
|
|
|
|
|
|
# send confirmed reply |
|
|
|
|
|
r = await self.sendgettest(l.send_enc( |
|
|
|
|
|
b'confirmed') + l.send_mac(8)) |
|
|
|
|
|
|
|
|
|
|
|
# test and decode remaining command messages |
|
|
|
|
|
cmd = l.recv_enc(r[:-8]) |
|
|
|
|
|
l.recv_mac(r[-8:]) |
|
|
|
|
|
|
|
|
|
|
|
assert cmd[0] == CMD_WAITFOR |
|
|
|
|
|
assert int.from_bytes(cmd[1:], |
|
|
|
|
|
byteorder='little') == 30 |
|
|
|
|
|
|
|
|
|
|
|
r = await self.sendgettest(l.send_enc( |
|
|
|
|
|
cmd[0:1]) + l.send_mac(8)) |
|
|
|
|
|
|
|
|
|
|
|
cmd = l.recv_enc(r[:-8]) |
|
|
|
|
|
l.recv_mac(r[-8:]) |
|
|
|
|
|
|
|
|
|
|
|
assert cmd[0] == CMD_RUNFOR |
|
|
|
|
|
assert int.from_bytes(cmd[1:5], |
|
|
|
|
|
byteorder='little') == 1 |
|
|
|
|
|
assert int.from_bytes(cmd[5:], |
|
|
|
|
|
byteorder='little') == 50 |
|
|
|
|
|
|
|
|
|
|
|
r = await self.sendgettest(l.send_enc( |
|
|
|
|
|
cmd[0:1]) + l.send_mac(8)) |
|
|
|
|
|
|
|
|
|
|
|
cmd = l.recv_enc(r[:-8]) |
|
|
|
|
|
l.recv_mac(r[-8:]) |
|
|
|
|
|
|
|
|
|
|
|
assert cmd[0] == CMD_TERMINATE |
|
|
|
|
|
|
|
|
|
|
|
await self.put(l.send_enc(cmd[0:1]) + |
|
|
|
|
|
l.send_mac(8)) |
|
|
|
|
|
|
|
|
|
|
|
tsd = TestSD() |
|
|
|
|
|
|
|
|
|
|
|
# make sure it fails w/o both specified |
|
|
|
|
|
with self.assertRaises(RuntimeError): |
|
|
|
|
|
l = LORANode(tsd, init_key=initkey) |
|
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(RuntimeError): |
|
|
|
|
|
l = LORANode(tsd, resp_pub=respkey.getpub()) |
|
|
|
|
|
|
|
|
|
|
|
l = LORANode(tsd, init_key=initkey, resp_pub=respkey.getpub()) |
|
|
|
|
|
|
|
|
|
|
|
await l.start() |
|
|
|
|
|
|
|
|
|
|
|
await l.waitfor(30) |
|
|
|
|
|
|
|
|
|
|
|
await l.runfor(1, 50) |
|
|
|
|
|
|
|
|
|
|
|
await l.terminate() |
|
|
|
|
|
|
|
|
|
|
|
await tsd.drain() |
|
|
|
|
|
|
|
|
|
|
|
# Make sure all messages have been processed |
|
|
|
|
|
self.assertTrue(tsd.sendq.empty()) |
|
|
|
|
|
self.assertTrue(tsd.recvq.empty()) |
|
|
|
|
|
#_debprint('done') |
|
|
|
|
|
|
|
|
@timeout(2) |
|
|
@timeout(2) |
|
|
async def test_lora_shared(self): |
|
|
async def test_lora_shared(self): |
|
|
_self = self |
|
|
_self = self |
|
|