Implement a secure ICS protocol targeting LoRa Node151 microcontroller for controlling irrigation.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

358 lines
8.5 KiB

  1. import asyncio
  2. import functools
  3. import os
  4. import unittest
  5. from Strobe.Strobe import Strobe, KeccakF
  6. from Strobe.Strobe import AuthenticationFailed
  7. import lora_comms
  8. from lora_comms import make_pktbuf
  9. domain = b'com.funkthat.lora.irrigation.shared.v0.0.1'
  10. # Response to command will be the CMD and any arguments if needed.
  11. # The command is encoded as an unsigned byte
  12. CMD_TERMINATE = 1 # no args: terminate the sesssion, reply confirms
  13. # The follow commands are queue up, but will be acknoledged when queued
  14. CMD_WAITFOR = 2 # arg: (length): waits for length seconds
  15. CMD_RUNFOR = 3 # arg: (chan, length): turns on chan for length seconds
  16. class LORANode(object):
  17. '''Implement a LORANode initiator.'''
  18. def __init__(self, syncdatagram, shared=None):
  19. self.sd = syncdatagram
  20. self.st = Strobe(domain, F=KeccakF(800))
  21. if shared is not None:
  22. self.st.key(shared)
  23. async def start(self):
  24. msg = self.st.send_enc(os.urandom(16) + b'reqreset') + \
  25. self.st.send_mac(8)
  26. resp = await self.sd.sendtillrecv(msg, 1)
  27. self.st.recv_enc(resp[:16])
  28. self.st.recv_mac(resp[16:])
  29. self.st.ratchet()
  30. resp = await self.sd.sendtillrecv(
  31. self.st.send_enc(b'confirm') + self.st.send_mac(8), 1)
  32. pkt = self.st.recv_enc(resp[:9])
  33. self.st.recv_mac(resp[9:])
  34. if pkt != b'confirmed':
  35. raise RuntimeError
  36. @staticmethod
  37. def _encodeargs(*args):
  38. r = []
  39. for i in args:
  40. r.append(i.to_bytes(4, byteorder='little'))
  41. return b''.join(r)
  42. async def _sendcmd(self, cmd, *args):
  43. cmdbyte = cmd.to_bytes(1, byteorder='little')
  44. pkt = await self.sd.sendtillrecv(
  45. self.st.send_enc(cmdbyte +
  46. self._encodeargs(*args)) + self.st.send_mac(8), 1)
  47. resp = self.st.recv_enc(pkt[:-8])
  48. self.st.recv_mac(pkt[-8:])
  49. if resp[0:1] != cmdbyte:
  50. raise RuntimeError('response does not match, got: %s, expected: %s' % (repr(resp[0:1]), repr(cmdbyte)))
  51. async def waitfor(self, length):
  52. return await self._sendcmd(CMD_WAITFOR, length)
  53. async def runfor(self, chan, length):
  54. return await self._sendcmd(CMD_RUNFOR, chan, length)
  55. async def terminate(self):
  56. return await self._sendcmd(CMD_TERMINATE)
  57. class SyncDatagram(object):
  58. '''Base interface for a more simple synchronous interface.'''
  59. def __init__(self): #pragma: no cover
  60. pass
  61. async def recv(self, timeout=None): #pragma: no cover
  62. '''Receive a datagram. If timeout is not None, wait that many
  63. seconds, and if nothing is received in that time, raise an TimeoutError
  64. exception.'''
  65. raise NotImplementedError
  66. async def send(self, data): #pragma: no cover
  67. '''Send a datagram.'''
  68. raise NotImplementedError
  69. async def sendtillrecv(self, data, freq):
  70. '''Send the datagram in data, every freq seconds until a datagram
  71. is received. If timeout seconds happen w/o receiving a datagram,
  72. then raise an TimeoutError exception.'''
  73. while True:
  74. await self.send(data)
  75. try:
  76. return await self.recv(freq)
  77. except TimeoutError:
  78. pass
  79. class MockSyncDatagram(SyncDatagram):
  80. '''A testing version of SyncDatagram. Define a method runner which
  81. implements part of the sequence. In the function, await on either
  82. self.get, to wait for the other side to send something, or await
  83. self.put w/ data to send.'''
  84. def __init__(self):
  85. self.sendq = asyncio.Queue()
  86. self.recvq = asyncio.Queue()
  87. self.task = None
  88. self.task = asyncio.create_task(self.runner())
  89. self.get = self.sendq.get
  90. self.put = self.recvq.put
  91. async def drain(self):
  92. '''Wait for the runner thread to finish up.'''
  93. return await self.task
  94. async def runner(self): #pragma: no cover
  95. raise NotImplementedError
  96. async def recv(self, timeout=None):
  97. return await self.recvq.get()
  98. async def send(self, data):
  99. return await self.sendq.put(data)
  100. def __del__(self): #pragma: no cover
  101. if self.task is not None and not self.task.done():
  102. self.task.cancel()
  103. class TestSyncData(unittest.IsolatedAsyncioTestCase):
  104. async def test_syncsendtillrecv(self):
  105. class MySync(SyncDatagram):
  106. def __init__(self):
  107. self.sendq = []
  108. self.resp = [ TimeoutError(), b'a' ]
  109. async def recv(self, timeout=None):
  110. assert timeout == 1
  111. r = self.resp.pop(0)
  112. if isinstance(r, Exception):
  113. raise r
  114. return r
  115. async def send(self, data):
  116. self.sendq.append(data)
  117. ms = MySync()
  118. r = await ms.sendtillrecv(b'foo', 1)
  119. self.assertEqual(r, b'a')
  120. self.assertEqual(ms.sendq, [ b'foo', b'foo' ])
  121. def timeout(timeout):
  122. def timeout_wrapper(fun):
  123. @functools.wraps(fun)
  124. async def wrapper(*args, **kwargs):
  125. return await asyncio.wait_for(fun(*args, **kwargs),
  126. timeout)
  127. return wrapper
  128. return timeout_wrapper
  129. class TestLORANode(unittest.IsolatedAsyncioTestCase):
  130. @timeout(2)
  131. async def test_lora(self):
  132. shared_key = os.urandom(32)
  133. class TestSD(MockSyncDatagram):
  134. async def runner(self):
  135. l = Strobe(domain, F=KeccakF(800))
  136. l.key(shared_key)
  137. # start handshake
  138. r = await self.get()
  139. pkt = l.recv_enc(r[:-8])
  140. l.recv_mac(r[-8:])
  141. assert pkt.endswith(b'reqreset')
  142. await self.put(l.send_enc(os.urandom(16)) +
  143. l.send_mac(8))
  144. l.ratchet()
  145. r = await self.get()
  146. c = l.recv_enc(r[:-8])
  147. l.recv_mac(r[-8:])
  148. assert c == b'confirm'
  149. await self.put(l.send_enc(b'confirmed') +
  150. l.send_mac(8))
  151. r = await self.get()
  152. cmd = l.recv_enc(r[:-8])
  153. l.recv_mac(r[-8:])
  154. assert cmd[0] == CMD_WAITFOR
  155. assert int.from_bytes(cmd[1:], byteorder='little') == 30
  156. await self.put(l.send_enc(cmd[0:1]) +
  157. l.send_mac(8))
  158. r = await self.get()
  159. cmd = l.recv_enc(r[:-8])
  160. l.recv_mac(r[-8:])
  161. assert cmd[0] == CMD_RUNFOR
  162. assert int.from_bytes(cmd[1:5], byteorder='little') == 1
  163. assert int.from_bytes(cmd[5:], byteorder='little') == 50
  164. await self.put(l.send_enc(cmd[0:1]) +
  165. l.send_mac(8))
  166. r = await self.get()
  167. cmd = l.recv_enc(r[:-8])
  168. l.recv_mac(r[-8:])
  169. assert cmd[0] == CMD_TERMINATE
  170. await self.put(l.send_enc(cmd[0:1]) +
  171. l.send_mac(8))
  172. tsd = TestSD()
  173. l = LORANode(tsd, shared=shared_key)
  174. await l.start()
  175. await l.waitfor(30)
  176. await l.runfor(1, 50)
  177. await l.terminate()
  178. await tsd.drain()
  179. # Make sure all messages have been processed
  180. self.assertTrue(tsd.sendq.empty())
  181. self.assertTrue(tsd.recvq.empty())
  182. @timeout(2)
  183. async def test_ccode(self):
  184. _self = self
  185. from ctypes import pointer, sizeof, c_uint8
  186. # seed the RNG
  187. prngseed = b'abc123'
  188. lora_comms.strobe_seed_prng((c_uint8 *
  189. len(prngseed))(*prngseed), len(prngseed))
  190. # Create the state for testing
  191. commstate = lora_comms.CommsState()
  192. # These are the expected messages and their arguments
  193. exptmsgs = [
  194. (CMD_WAITFOR, [ 30 ]),
  195. (CMD_RUNFOR, [ 1, 50 ]),
  196. (CMD_TERMINATE, [ ]),
  197. ]
  198. def procmsg(msg, outbuf):
  199. msgbuf = msg._from()
  200. #print('procmsg:', repr(msg), repr(msgbuf), repr(outbuf))
  201. cmd = msgbuf[0]
  202. args = [ int.from_bytes(msgbuf[x:x + 4],
  203. byteorder='little') for x in range(1, len(msgbuf),
  204. 4) ]
  205. if exptmsgs[0] == (cmd, args):
  206. exptmsgs.pop(0)
  207. outbuf[0].pkt[0] = cmd
  208. outbuf[0].pktlen = 1
  209. else: #pragma: no cover
  210. raise RuntimeError('cmd not found')
  211. # wrap the callback function
  212. cb = lora_comms.process_msgfunc_t(procmsg)
  213. class CCodeSD(MockSyncDatagram):
  214. async def runner(self):
  215. for expectlen in [ 24, 17, 9, 9, 9 ]:
  216. # get message
  217. gb = await self.get()
  218. r = make_pktbuf(gb)
  219. outbytes = bytearray(64)
  220. outbuf = make_pktbuf(outbytes)
  221. # process the test message
  222. lora_comms.comms_process(commstate, r,
  223. outbuf)
  224. # make sure the reply matches length
  225. _self.assertEqual(expectlen,
  226. outbuf.pktlen)
  227. # save what was originally replied
  228. origmsg = outbuf._from()
  229. # pretend that the reply didn't make it
  230. r = make_pktbuf(gb)
  231. outbuf = make_pktbuf(outbytes)
  232. lora_comms.comms_process(commstate, r,
  233. outbuf)
  234. # make sure that the reply matches previous
  235. _self.assertEqual(origmsg, outbuf._from())
  236. # pass the reply back
  237. await self.put(outbytes[:outbuf.pktlen])
  238. # Generate shared key
  239. shared_key = os.urandom(32)
  240. # Initialize everything
  241. lora_comms.comms_init(commstate, cb, make_pktbuf(shared_key))
  242. # Create test fixture
  243. tsd = CCodeSD()
  244. l = LORANode(tsd, shared=shared_key)
  245. # Send various messages
  246. await l.start()
  247. await l.waitfor(30)
  248. await l.runfor(1, 50)
  249. await l.terminate()
  250. await tsd.drain()
  251. # Make sure all messages have been processed
  252. self.assertTrue(tsd.sendq.empty())
  253. self.assertTrue(tsd.recvq.empty())
  254. # Make sure all the expected messages have been
  255. # processed.
  256. self.assertFalse(exptmsgs)