diff --git a/comms.c b/comms.c index f66ce53..4d809ab 100644 --- a/comms.c +++ b/comms.c @@ -29,7 +29,8 @@ 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"; +static const uint8_t shared_domain[] = "com.funkthat.lora.irrigation.shared.v0.0.1"; +static const uint8_t ecdhe_domain[] = "com.funkthat.lora.irrigation.ecdhe.v0.0.1"; static const uint8_t reqreset[] = "reqreset"; static const uint8_t confirm[] = "confirm"; @@ -60,25 +61,55 @@ _strobe_state_size() return sizeof(strobe_s); } -void -comms_init(struct comms_state *cs, process_msgfunc_t pmf, struct pktbuf *shared) +int +comms_init(struct comms_state *cs, process_msgfunc_t pmf, + struct pktbuf *shared, struct pktbuf *resp_key, struct pktbuf *init_pubkey) { + unsigned char buf[EC_PUBLIC_BYTES * 2]; + strobe_s *crypt; *cs = (struct comms_state){ .cs_procmsg = pmf, }; - strobe_init(&cs->cs_start, domain, sizeof domain - 1); + crypt = &cs->cs_start.cs_crypto; + + if (shared != NULL) { + strobe_init(crypt, shared_domain, sizeof shared_domain - 1); + + strobe_key(crypt, SYM_KEY, shared->pkt, shared->pktlen); + + cs->cs_start.cs_state = COMMS_WAIT_REQUEST_SHARED; + } else if (resp_key != NULL && init_pubkey != NULL) { + strobe_init(crypt, ecdhe_domain, sizeof ecdhe_domain - 1); + + if (resp_key->pktlen != EC_PRIVATE_BYTES || + init_pubkey->pktlen != EC_PUBLIC_BYTES) + return 0; + + /* store our private key */ + memcpy(cs->cs_respkey, resp_key->pkt, resp_key->pktlen); + x25519_base(cs->cs_resppubkey, resp_key->pkt, 1); + + /* store the public key to associate with */ + memcpy(cs->cs_initpubkey, init_pubkey->pkt, init_pubkey->pktlen); + + /* public keys */ + memcpy(&buf[0], cs->cs_initpubkey, EC_PUBLIC_BYTES); + memcpy(&buf[EC_PUBLIC_BYTES], cs->cs_resppubkey, EC_PUBLIC_BYTES); + + strobe_key(crypt, SYM_KEY, buf, EC_PUBLIC_BYTES * 2); - if (shared != NULL) - strobe_key(&cs->cs_start, SYM_KEY, shared->pkt, shared->pktlen); + cs->cs_start.cs_state = COMMS_WAIT_REQUEST_ECDHE; + } else { + return 0; + } /* copy starting state over to initial state */ - cs->cs_active = (struct comms_session){ - .cs_crypto = cs->cs_start, - .cs_state = COMMS_WAIT_REQUEST, - }; + cs->cs_active = cs->cs_start; cs->cs_pending = cs->cs_active; + + return 1; } #define CONFIRMED_STR_BASE "confirmed" @@ -128,7 +159,7 @@ badmsg: ret = 0; switch (sess->cs_state) { - case COMMS_WAIT_REQUEST: + case COMMS_WAIT_REQUEST_SHARED: if (msglen != 24 || memcmp(reqreset, &buf[16], sizeof reqreset - 1) != 0) goto badmsg; @@ -143,6 +174,55 @@ badmsg: sess->cs_state = COMMS_WAIT_CONFIRM; break; + case COMMS_WAIT_REQUEST_ECDHE: + { + unsigned char ephkey[EC_PRIVATE_BYTES]; + unsigned char ephpubkey[EC_PUBLIC_BYTES]; + unsigned char keybuf[EC_PUBLIC_BYTES * 2]; + + if (msglen != (EC_PUBLIC_BYTES + 8) || memcmp(reqreset, &buf[EC_PUBLIC_BYTES], + sizeof reqreset - 1) != 0) + goto badmsg; + + /* DH(s, re) */ + memcpy(&keybuf[0], cs->cs_resppubkey, EC_PUBLIC_BYTES); + if (x25519(&keybuf[0], cs->cs_respkey, buf, 1) != 0) + goto badmsg; + + /* DH(s, rs) */ + memcpy(&keybuf[EC_PUBLIC_BYTES], cs->cs_resppubkey, EC_PUBLIC_BYTES); + if (x25519(&keybuf[EC_PUBLIC_BYTES], cs->cs_respkey, cs->cs_initpubkey, 1) != 0) + goto badmsg; + + /* key(DH() + DH()) */ + strobe_key(&sess->cs_crypto, SYM_KEY, keybuf, EC_PUBLIC_BYTES * 2); + + /* generate ephemeral */ + bare_strobe_randomize(ephkey, EC_PRIVATE_BYTES); + if (x25519_base(ephpubkey, ephkey, 1) != 0) + goto badmsg; + + ret = strobe_put(&sess->cs_crypto, APP_CIPHERTEXT, ephpubkey, + EC_PUBLIC_BYTES); + ret += strobe_put(&sess->cs_crypto, MAC, NULL, MAC_LEN); + + /* DH(e, re) */ + memcpy(&keybuf[0], ephpubkey, EC_PUBLIC_BYTES); + if (x25519(&keybuf[0], ephkey, buf, 1) != 0) + goto badmsg; + + /* DH(e, rs) */ + memcpy(&keybuf[EC_PUBLIC_BYTES], ephpubkey, EC_PUBLIC_BYTES); + if (x25519(&keybuf[EC_PUBLIC_BYTES], ephkey, cs->cs_initpubkey, 1) != 0) + goto badmsg; + + /* key(DH() + DH()) */ + strobe_key(&sess->cs_crypto, SYM_KEY, keybuf, EC_PUBLIC_BYTES * 2); + + sess->cs_state = COMMS_WAIT_CONFIRM; + break; + } + case COMMS_WAIT_CONFIRM: if (msglen != 7 || memcmp(confirm, buf, sizeof confirm - 1) != 0) @@ -227,20 +307,14 @@ retmsg: if (cs->cs_pending.cs_state == COMMS_PROCESS_MSGS) { /* new active state */ cs->cs_active = cs->cs_pending; - cs->cs_pending = (struct comms_session){ - .cs_crypto = cs->cs_start, - .cs_state = COMMS_WAIT_REQUEST, - }; + cs->cs_pending = cs->cs_start; goto retmsg; } /* pending session didn't work, maybe new */ struct comms_session tmpsess; - tmpsess = (struct comms_session){ - .cs_crypto = cs->cs_start, - .cs_state = COMMS_WAIT_REQUEST, - }; + tmpsess = cs->cs_start; pbouttmp = *pbout; _comms_process_session(cs, &tmpsess, pbin, &pbouttmp); diff --git a/comms.h b/comms.h index b4e71dc..a2aed29 100644 --- a/comms.h +++ b/comms.h @@ -28,6 +28,7 @@ #include #include +#include #define COMMS_MAXMSG 64 @@ -40,7 +41,8 @@ struct pktbuf { typedef void (*process_msgfunc_t)(struct pktbuf, struct pktbuf *); enum comm_state { - COMMS_WAIT_REQUEST = 1, + COMMS_WAIT_REQUEST_SHARED = 1, + COMMS_WAIT_REQUEST_ECDHE, COMMS_WAIT_CONFIRM, COMMS_PROCESS_MSGS, }; @@ -72,7 +74,11 @@ struct comms_state { struct comms_session cs_active; /* current active session */ struct comms_session cs_pending; /* current pending session */ - strobe_s cs_start; /* special starting state cache */ + unsigned char cs_respkey[EC_PRIVATE_BYTES]; /* private key for device */ + unsigned char cs_resppubkey[EC_PUBLIC_BYTES]; /* public key for device */ + unsigned char cs_initpubkey[EC_PUBLIC_BYTES]; /* public key for initiator */ + + struct comms_session cs_start; /* special starting state cache */ process_msgfunc_t cs_procmsg; @@ -86,5 +92,5 @@ struct comms_state { size_t _strobe_state_size(); size_t _comms_state_size(); -void comms_init(struct comms_state *, process_msgfunc_t, struct pktbuf *); +int comms_init(struct comms_state *, process_msgfunc_t, struct pktbuf *, struct pktbuf *, struct pktbuf *); void comms_process(struct comms_state *, struct pktbuf, struct pktbuf *); diff --git a/lora.py b/lora.py index 24785da..f3507c3 100644 --- a/lora.py +++ b/lora.py @@ -929,7 +929,7 @@ class TestLORANode(unittest.IsolatedAsyncioTestCase): shared_key = os.urandom(32) # Initialize everything - lora_comms.comms_init(commstate, cb, make_pktbuf(shared_key)) + lora_comms.comms_init(commstate, cb, make_pktbuf(shared_key), None, None) # Create test fixture, only use it to init crypto state tsd = SyncDatagram() @@ -996,6 +996,103 @@ class TestLORANode(unittest.IsolatedAsyncioTestCase): self.assertFalse(out) + @timeout(2) + async def test_ccode_ecdhe(self): + _self = self + from ctypes import 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_PING, [ ]), + (CMD_TERMINATE, [ ]), + ] + def procmsg(msg, outbuf): + msgbuf = msg._from() + 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 [ 40, 17, 9, 9, 9, 9 ]: + # get message + inmsg = await self.get() + + # process the test message + out = lora_comms.comms_process_wrap( + commstate, inmsg) + + # make sure the reply matches length + _self.assertEqual(expectlen, len(out)) + + # save what was originally replied + origmsg = out + + # pretend that the reply didn't make it + out = lora_comms.comms_process_wrap( + commstate, inmsg) + + # make sure that the reply matches + # the previous + _self.assertEqual(origmsg, out) + + # pass the reply back + await self.put(out) + + # Generate keys + initkey = X25519.gen() + respkey = X25519.gen() + + # Initialize everything + lora_comms.comms_init(commstate, cb, None, make_pktbuf(respkey.getpriv()), make_pktbuf(initkey.getpub())) + + # Create test fixture + tsd = CCodeSD() + l = LORANode(tsd, init_key=initkey, resp_pub=respkey.getpub()) + + # Send various messages + await l.start() + + await l.waitfor(30) + + await l.runfor(1, 50) + + await l.ping() + + 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) + #_debprint('done') + @timeout(2) async def test_ccode(self): _self = self @@ -1064,7 +1161,7 @@ class TestLORANode(unittest.IsolatedAsyncioTestCase): shared_key = os.urandom(32) # Initialize everything - lora_comms.comms_init(commstate, cb, make_pktbuf(shared_key)) + lora_comms.comms_init(commstate, cb, make_pktbuf(shared_key), None, None) # Create test fixture tsd = CCodeSD() @@ -1193,7 +1290,7 @@ class TestLORANode(unittest.IsolatedAsyncioTestCase): shared_key = os.urandom(32) # Initialize everything - lora_comms.comms_init(commstate, cb, make_pktbuf(shared_key)) + lora_comms.comms_init(commstate, cb, make_pktbuf(shared_key), None, None) # Create test fixture tsd = CCodeSD1() @@ -1276,7 +1373,7 @@ class TestLoRaNodeMulticast(unittest.IsolatedAsyncioTestCase): shared_key = os.urandom(32) # Initialize everything - lora_comms.comms_init(commstate, cb, make_pktbuf(shared_key)) + lora_comms.comms_init(commstate, cb, make_pktbuf(shared_key), None, None) # create the object we are testing msd = MulticastSyncDatagram(self.maddr) diff --git a/lora_comms.py b/lora_comms.py index e757e1a..95a19c9 100644 --- a/lora_comms.py +++ b/lora_comms.py @@ -83,12 +83,21 @@ class CommsSession(Structure,StructureRepr): ('cs_state', c_int), ] +EC_PUBLIC_BYTES = 32 +EC_PRIVATE_BYTES = 32 + class CommsState(Structure,StructureRepr): _fields_ = [ # The alignment of these may be off ('cs_active', CommsSession), ('cs_pending', CommsSession), - ('cs_start', c_uint64 * _strobe_state_u64_cnt), + + ('cs_respkey', c_uint8 * EC_PRIVATE_BYTES), + ('cs_resppubkey', c_uint8 * EC_PUBLIC_BYTES), + ('cs_initpubkey', c_uint8 * EC_PUBLIC_BYTES), + + ('cs_start', CommsSession), + ('cs_procmsg', process_msgfunc_t), ('cs_prevmsg', PktBuf), @@ -98,9 +107,6 @@ class CommsState(Structure,StructureRepr): ('cs_prevmsgrespbuf', c_uint8 * 64), ] -EC_PUBLIC_BYTES = 32 -EC_PRIVATE_BYTES = 32 - if _lib is not None: _lib._comms_state_size.restype = c_size_t _lib._comms_state_size.argtypes = () @@ -111,8 +117,8 @@ if _lib is not None: X25519_BASE_POINT = (c_uint8 * (256//8)).in_dll(_lib, 'X25519_BASE_POINT') for func, ret, args in [ - ('comms_init', None, (POINTER(CommsState), process_msgfunc_t, - POINTER(PktBuf))), + ('comms_init', c_int, (POINTER(CommsState), process_msgfunc_t, + POINTER(PktBuf), POINTER(PktBuf), POINTER(PktBuf))), ('comms_process', None, (POINTER(CommsState), PktBuf, POINTER(PktBuf))), ('strobe_seed_prng', None, (POINTER(c_uint8), c_ssize_t)), ('x25519', c_int, (c_uint8 * EC_PUBLIC_BYTES, c_uint8 * EC_PRIVATE_BYTES, c_uint8 * EC_PUBLIC_BYTES, c_int)),