diff --git a/Makefile b/Makefile index 60666b3..04025f3 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,14 @@ ARMTARGET?= -mcpu=cortex-m3 -mthumb -DSTROBE_SINGLE_THREAD=1 #ARMCC?=clang-mp-9.0 #ARMTARGET?= -nostdlib -ffreestanding -target arm-none-eabi -mcpu=cortex-m3 -mfloat-abi=soft -mthumb +PLATFORM != uname -s + +.if $(PLATFORM) == "Darwin" +SOEXT=dylib +.else +.error Unsupported platform: $(PLATFORM) +.endif + PROG = lora.irr PROGEXT = .elf @@ -86,12 +94,28 @@ CFLAGS+= -I$(STM32)/usb OBJS = $(SRCS:C/.c$/.o/) CFLAGS+= -Werror -Wall +LIBLORA_TEST_SRCS= comms.c strobe.c x25519.c +LIBLORA_TEST_OBJS= $(LIBLORA_TEST_SRCS:C/.c$/.no/) + +LIBLORA_TEST = liblora_test.$(SOEXT) +$(LIBLORA_TEST): $(LIBLORA_TEST_OBJS) + $(CC) -shared -o $@ $(.ALLSRC) + +.MAIN: all .PHONY: all all: $(PROG)$(PROGEXT) $(PROG).list .PHONY: depend -depend: $(SRCS) - $(ARMCC) $(ARMTARGET) $(CFLAGS) $(.ALLSRC) -MM > .depend || rm -f .depend +depend: .arm_deps .test_deps + +.sinclude ".arm_deps" +.sinclude ".test_deps" + +.arm_deps: $(SRCS) + $(ARMCC) $(ARMTARGET) $(CFLAGS) $(.ALLSRC) -MM > $@ || rm -f $@ + +.test_deps: $(LIBLORA_TEST_SRCS) + $(CC) $(CFLAGS) $(.ALLSRC) -MM | sed -e 's/\.o:/\.no:/' > $@ || rm -f $@ $(PROG)$(PROGEXT): $(OBJS) $(ARMCC) $(ARMTARGET) -o $@ $(.ALLSRC) -T$(LINKER_SCRIPT) --specs=nosys.specs -Wl,--gc-sections -static --specs=nano.specs -Wl,--start-group -lc -lm -Wl,--end-group @@ -104,8 +128,13 @@ runbuild: $(SRCS) for i in $(.MAKEFILE_LIST) $(.ALLSRC) $$(gsed ':x; /\\$$/ { N; s/\\\n//; tx }' < .depend | sed -e 's/^[^:]*://'); do if [ "$$i" != ".." ]; then echo $$i; fi; done | entr -d sh -c 'echo starting...; cd $(.CURDIR) && $(MAKE) $(.MAKEFLAGS) depend && $(MAKE) $(.MAKEFLAGS) all' .PHONY: runtests -runtests: lora.py - ls $(.ALLSRC) | entr sh -c 'PYTHONPATH="$(.CURDIR)" python -m coverage run -m unittest lora && coverage report --omit=p/\* -m -i' +runtests: Makefile lora_comms.py lora.py $(LIBLORA_TEST) $(LIBLORA_TEST_SRCS) + ls $(.ALLSRC) | entr sh -c '(cd $(.CURDIR) && $(MAKE) $(.MAKEFLAGS) $(LIBLORA_TEST)) && ((PYTHONPATH="$(.CURDIR)" python -m coverage run -m unittest lora && coverage report --omit=p/\* -m -i) 2>&1 | head -n 20)' + +# native objects +.SUFFIXES: .no +.c.no: + $(CC) $(CFLAGS) -c $< -o $@ .c.o: $(ARMCC) $(ARMTARGET) $(CFLAGS) -c $< -o $@ diff --git a/comms.c b/comms.c new file mode 100644 index 0000000..2ae25a8 --- /dev/null +++ b/comms.c @@ -0,0 +1,110 @@ +#include +#include + +static const size_t MAC_LEN = 8; +static const size_t CHALLENGE_LEN = 16; +static const uint8_t domain[] = "com.funkthat.lora.irrigation.shared.v0.0.1"; + +size_t +_strobe_state_size() +{ + + return sizeof(strobe_s); +} + +void +comms_init(struct comms_state *cs, process_msgfunc_t pmf) +{ + + *cs = (struct comms_state){ + .cs_comm_state = COMMS_WAIT_REQUEST, + .cs_procmsg = pmf, + }; + + strobe_init(&cs->cs_start, domain, sizeof domain - 1); + + /* copy starting state over to initial state */ + cs->cs_state = cs->cs_start; + +} + +#define CONFIRMED_STR_BASE "confirmed" +#define CONFIRMED_STR ((const uint8_t *)CONFIRMED_STR_BASE) +#define CONFIRMED_STR_LEN (sizeof(CONFIRMED_STR_BASE) - 1) + +/* + * encrypted data to be processed is passed in via pbin. + * + * The pktbuf pointed to by pbout contains the buffer that a [encrypted] + * response will be written to. The length needs to be updated, where 0 + * means no reply. + */ +void +comms_process(struct comms_state *cs, struct pktbuf pbin, struct pktbuf *pbout) +{ + uint8_t buf[64] = {}; + struct pktbuf pbmsg, pbrep; + ssize_t cnt, ret, msglen; + + strobe_attach_buffer(&cs->cs_state, pbin.pkt, pbin.pktlen); + + cnt = strobe_get(&cs->cs_state, APP_CIPHERTEXT, buf, pbin.pktlen - + MAC_LEN); + msglen = cnt; + + cnt = strobe_get(&cs->cs_state, MAC, pbin.pkt + + (pbin.pktlen - MAC_LEN), MAC_LEN); + + /* XXX - cnt != MAC_LEN test case */ + + /* + * if we have arrived here, MAC has been verified, and buf now + * contains the data to operate upon. + */ + + /* attach the buffer for output */ + strobe_attach_buffer(&cs->cs_state, pbout->pkt, pbout->pktlen); + + ret = 0; + switch (cs->cs_comm_state) { + case COMMS_WAIT_REQUEST: + /* XXX - reqreset check */ + + bare_strobe_randomize(buf, CHALLENGE_LEN); + ret = strobe_put(&cs->cs_state, APP_CIPHERTEXT, buf, + CHALLENGE_LEN); + ret += strobe_put(&cs->cs_state, MAC, NULL, MAC_LEN); + cs->cs_comm_state = COMMS_WAIT_CONFIRM; + break; + + case COMMS_WAIT_CONFIRM: + /* XXX - confirm check */ + ret = strobe_put(&cs->cs_state, APP_CIPHERTEXT, CONFIRMED_STR, + CONFIRMED_STR_LEN); + ret += strobe_put(&cs->cs_state, MAC, NULL, MAC_LEN); + cs->cs_comm_state = COMMS_PROCESS_MSGS; + break; + + case COMMS_PROCESS_MSGS: { + uint8_t repbuf[pbout->pktlen - MAC_LEN]; + + memset(repbuf, '\x00', sizeof repbuf); + + pbmsg.pkt = buf; + pbmsg.pktlen = msglen; + + pbrep.pkt = repbuf; + pbrep.pktlen = sizeof repbuf; + + cs->cs_procmsg(pbmsg, &pbrep); + + ret = strobe_put(&cs->cs_state, APP_CIPHERTEXT, repbuf, + pbrep.pktlen); + ret += strobe_put(&cs->cs_state, MAC, NULL, MAC_LEN); + + break; + } + } + + pbout->pktlen = ret; +} diff --git a/comms.h b/comms.h new file mode 100644 index 0000000..e53062d --- /dev/null +++ b/comms.h @@ -0,0 +1,31 @@ +#include +#include + +#include + +struct pktbuf { + uint8_t *pkt; + uint16_t pktlen; +}; + +/* first arg is input buffer, second arg is what will be sent as reply */ +typedef void (*process_msgfunc_t)(struct pktbuf, struct pktbuf *); + +enum comm_state { + COMMS_WAIT_REQUEST = 1, + COMMS_WAIT_CONFIRM, + COMMS_PROCESS_MSGS, +}; + +struct comms_state { + strobe_s cs_state; + enum comm_state cs_comm_state; + strobe_s cs_start; /* special starting state cache */ + + process_msgfunc_t cs_procmsg; +}; + +size_t _strobe_state_size(); + +void comms_init(struct comms_state *, process_msgfunc_t); +void comms_process(struct comms_state *, struct pktbuf, struct pktbuf *); diff --git a/lora.py b/lora.py index 94c6ee9..f34bfa7 100644 --- a/lora.py +++ b/lora.py @@ -3,9 +3,12 @@ import functools import os import unittest -from Strobe.Strobe import Strobe +from Strobe.Strobe import Strobe, KeccakF from Strobe.Strobe import AuthenticationFailed +import lora_comms +from lora_comms import make_pktbuf + domain = b'com.funkthat.lora.irrigation.shared.v0.0.1' # Response to command will be the CMD and any arguments if needed. @@ -21,7 +24,7 @@ class LORANode(object): def __init__(self, syncdatagram): self.sd = syncdatagram - self.st = Strobe(domain) + self.st = Strobe(domain, F=KeccakF(800)) async def start(self): msg = self.st.send_enc(os.urandom(16) + b'reqreset') + \ @@ -174,7 +177,7 @@ class TestLORANode(unittest.IsolatedAsyncioTestCase): async def test_lora(self): class TestSD(MockSyncDatagram): async def runner(self): - l = Strobe(domain) + l = Strobe(domain, F=KeccakF(800)) # start handshake r = await self.get() @@ -243,4 +246,86 @@ class TestLORANode(unittest.IsolatedAsyncioTestCase): self.assertTrue(tsd.sendq.empty()) self.assertTrue(tsd.recvq.empty()) - print('done') + @timeout(2) + async def test_ccode(self): + _self = self + from ctypes import pointer, sizeof, c_uint8 + + # seed the RNG + prngseed = b'abc123' + lora_comms.strobe_seed_prng((c_uint8 * + len(prngseed))(*prngseed), len(prngseed)) + + # Create the state for testing + commstate = lora_comms.CommsState() + + # These are the expected messages and their arguments + exptmsgs = [ + (CMD_WAITFOR, [ 30 ]), + (CMD_RUNFOR, [ 1, 50 ]), + (CMD_TERMINATE, [ ]), + ] + def procmsg(msg, outbuf): + msgbuf = msg._from() + #print('procmsg:', repr(msg), repr(msgbuf), repr(outbuf)) + cmd = msgbuf[0] + args = [ int.from_bytes(msgbuf[x:x + 4], + byteorder='little') for x in range(1, len(msgbuf), + 4) ] + + if exptmsgs[0] == (cmd, args): + exptmsgs.pop(0) + outbuf[0].pkt[0] = cmd + outbuf[0].pktlen = 1 + else: #pragma: no cover + raise RuntimeError('cmd not found') + + # wrap the callback function + cb = lora_comms.process_msgfunc_t(procmsg) + + class CCodeSD(MockSyncDatagram): + async def runner(self): + for expectlen in [ 24, 17, 9, 9, 9 ]: + # get message + gb = await self.get() + r = make_pktbuf(gb) + + outbytes = bytearray(64) + outbuf = make_pktbuf(outbytes) + + # process the test message + lora_comms.comms_process(commstate, r, + outbuf) + + # make sure the reply matches length + _self.assertEqual(expectlen, + outbuf.pktlen) + + # pass the reply back + await self.put(outbytes[:outbuf.pktlen]) + + # Initialize everything + lora_comms.comms_init(commstate, cb) + + # Create test fixture + tsd = CCodeSD() + l = LORANode(tsd) + + # Send various messages + 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()) + + # Make sure all the expected messages have been + # processed. + self.assertFalse(exptmsgs) diff --git a/lora_comms.py b/lora_comms.py new file mode 100644 index 0000000..b2830b6 --- /dev/null +++ b/lora_comms.py @@ -0,0 +1,59 @@ +from ctypes import Structure, POINTER, CFUNCTYPE, pointer +from ctypes import c_uint8, c_uint16, c_ssize_t, c_size_t, c_uint64 +from ctypes import CDLL + +class PktBuf(Structure): + _fields_ = [ + ('pkt', POINTER(c_uint8)), + ('pktlen', c_uint16), + ] + + def _from(self): + return bytes(self.pkt[:self.pktlen]) + + def __repr__(self): + return 'PktBuf(pkt=%s, pktlen=%s)' % (repr(self._from()), + self.pktlen) + +def make_pktbuf(s): + pb = PktBuf() + + if isinstance(s, bytearray): + obj = s + pb.pkt = pointer(c_uint8.from_buffer(s)) + #print('mp:', repr(pb.pkt)) + else: + obj = (c_uint8 * len(s))(*s) + pb.pkt = obj + + pb.pktlen = len(s) + + pb._make_pktbuf_ref = (obj, s) + + return pb + +process_msgfunc_t = CFUNCTYPE(None, PktBuf, POINTER(PktBuf)) + +_lib = CDLL('liblora_test.dylib') + +_lib._strobe_state_size.restype = c_size_t +_lib._strobe_state_size.argtypes = () +_strobe_state_u64_cnt = (_lib._strobe_state_size() + 7) // 8 + +class CommsState(Structure): + _fields_ = [ + # The alignment of these may be off + ('cs_state', c_uint64 * _strobe_state_u64_cnt), + ('cs_start', c_uint64 * _strobe_state_u64_cnt), + ('cs_procmsg', process_msgfunc_t), + ] + +for func, ret, args in [ + ('comms_init', None, (POINTER(CommsState), process_msgfunc_t)), + ('comms_process', None, (POINTER(CommsState), PktBuf, POINTER(PktBuf))), + ('strobe_seed_prng', None, (POINTER(c_uint8), c_ssize_t)), + ]: + f = getattr(_lib, func) + f.restype = ret + f.argtypes = args + locals()[func] = f diff --git a/strobe/strobe.c b/strobe/strobe.c index fb521cc..d77749c 100644 --- a/strobe/strobe.c +++ b/strobe/strobe.c @@ -224,25 +224,6 @@ static ssize_t strobe_operate_0 ( return -1; } - /* Read/write the control word */ - strobe_serialized_control_t str = { - GET_CONTROL_TAG(flags), - receiving_the_length ? 0 : eswap_htole_sl(len) - }; - if (!more) { - TRY(strobe_duplex(strobe, cwf, (uint8_t *)&str, sizeof(str.control) + length_bytes)); - } - str.len = eswap_letoh_sl(str.len); - - // Check received control word and length - if ( str.control != GET_CONTROL_TAG(flags) - || str.len > INT_MAX - || ((ssize_t)(len + str.len) > 0 && (ssize_t)str.len != len) - ) { - return -1; - } - len = str.len; - if (flags & FLAG_NO_DATA) return 0; return strobe_duplex(strobe, flags, inside, len);