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