projects coap fob

Mariano Alvira edited this page Jan 29, 2013 · 1 revision

6LoWPAN/CoAP Automobile Key Fob

Building a Open Source / Open Hardware wireless kill switch for your car

Written by: Mariano Alvira (mar@devl.org) 14 April 2011

System Overview

I drive a 1998 Jeep Wrangler and recently it was stolen. As it turns out, a Wangler is embarrassingly easy to steal. The thief used a screwdriver to forced the door lock and break out the ignition; without an ignition you can start the car with your finger.

Ignition1

Ignition2

Seeing how easy is was for someone to steal my car annoyed me quite a bit, so I looked into the best ways to protect your vehicle. In addition to an ignition (which on a Wrangler should be modified to make it harder to remove), it seems that a brake/steering wheel lock and a engine "kill switch" with a good hood lock seems to do the job; at least, it should provide enough motivation to move on to the next Jeep Wrangler that's parked 100 feet away.

This article focuses on the kill switch. There are many electrical ways to stop a car from starting. The idea of a kill switch is to choose one of these systems and interrupt it's operation. While the lowest tech way to do this is to pull a fuse or relay from the engine and lock the hood, it's also very inconvenient as you need to open the engine compartment every time you start the car or park it. With this in mind, it is highly desirable to make the kill switch wireless so that the engine can be enabled without messing under the hood.

Hardware

The hardware used are two Econotags from Redwire, LLC. The Econotags feature the MC13224V wireless microcontroller from Freescale. The MC13224V is an extremely capable ARM7 based CPU that's easy to use and fully supported by the Contiki Operating System; but more about Contiki later.

One of the nice features about the Econotag is that it has flexible power supply options. For the car-side econotag (the server), we power the Econotag from it's external regulated input (shown in the upper left):

Jeep

this input is accepts up to 16V and provides a regulated 3v3 to the Econotag. This input is connected to the car battery through a standard automotive fuse.

Jeep

The Econotag has been modified slightly to fit in a plastic project box on hand.

Jeep

The normal USB connector was removed and half of a regular USB cable was pigtailed into the USB connector vias on the Econotag.

Jeep

In the above picture you can also see a jumper that has been installed in JP4; the trace on the reverse side of this jumper has been cut with a Dremel. When the kill switch is installed in the Jeep, this jumper is placed an an "open" which disconnects the FT2232H and reduces power consumption.

The "key fob" is another econotag powered by an iRecharge IPR3 --- a rechargeable battery pack designed for iPod shuffles. The Econotag fits into them perfectly. You can pick up IPR3s for about $3 online.

Jeep

Software

The firmware running on both the kill switch and the fob is the Contiki OS. Contiki provides some great "standard" embedded OS features such as event based programming and concurrency (i.e. "processes"). Contiki also provides a full ipv4 and ipv6 stack as well as implementations of the latest 802.15.4 standards: 6lowpan, RPL, and CoAP.

  • 6lowpan: this is an adaption layer between 802.15.4 and ipv6 --- it lets you do "ipv6 over 802.15.4". As an ipv6 packet may be up to 1280 bytes long but the maximum payload for an 802.15.4 packet is 125 bytes, to do ipv6 over 802.15.4 link-level fragmentation must be implemented. This is the main problem 6lowpan solves. 6lowpan also addresses details regarding compression and addressing, and other nitty-gritty details.

  • RPL: this is the Routing protocol for Lossy-Networks; this provides mult-hop capability to your 6lowpan network. RPL uses a routing metric based on the connection quality of the node-to-node links to form routing trees so that packets from node A can get to B through the best set of intermediary nodes. Multihop isn't necessary or used in this application, but I thought I'd mention it anyway.

  • CoAP: is a "application layer" for constrained devices. It provides a set of methods similar to HTTP requests: i.e. GET, PUT, POST, and DELETE. While these methods are analogous to their HTTP cousins, they are implemented in a way more suitable for networks of constrained devices; instead of costly TCP connections, CoAP uses lightweight messages over UDP.

fob-server.c runs on the kill-switch inside the engine compartment and the coap-fob.c runs on the key fob. These have been adapted from the examples/rest-example client and server in the mainline Contiki source tree.

Fob-server

The fob-server provides a lock resource; the current state of the lock can be acquired with a GET request, and the state of the lock can be changed with a PUT or POST. These actions are all protected with a challenge/response that uses a secret key on each device and the AES hardware on the mc13224v. The fob-server provides a challenge resource:

GET the current challenge:

coap://[fe80::250:c2a8:ca00:0001]/challenge

Which will respond with the current challenge. The challenge is a random 128-bit AES-CTR counter and a random 128-bit number. The proper response is to use that counter, and the secret key, to encrypt the challenge number. The response is provided as a query variable to the protected resources. The challenge is changed on every request to a protected resource.

To GET the current lock status:

coap://[fe80::250:c2a8:ca00:0001]/lock?resp=481011e1324412c64fce41031d8cb557

On receiving this request, the server verifies that the challenge is correct. If it is, then it reports the status of the lock as "locked" or "unlocked". If the response is incorrect, then a "bad request" response is issued. In either case, a new challenge is generated.

Anatomy of a CoAP request handler

The following code starts the REST/CoAP subsystem and creates the lock and challenge resource handlers in Contiki:

rest_init();
rest_activate_resource(&resource_challenge);
rest_activate_resource(&resource_lock);

&resource_challenge and &resource_lock are function pointers created by the RESOURCE macros provided by rest.h; these macros initialize the resource handlers properly. The declaration for the challenge handler follows:

RESOURCE(challenge, METHOD_GET, "challenge");
void challenge_handler(REQUEST* request, RESPONSE* response)
{
  /* handler code here */
}

The call to the RESOURCE macro sets up a "challenge" resource with allowable methods as METHOD_GET. The fob-server keeps the current challenge in a global array. The handler code copies the challenge into the response payload, sets the content types, and issues the response:

volatile int i;
for(i = 0; i < 32; i++) {
      sprintf(temp + i*2, "%02x", challenge[i]);
}
temp[65] = 0;
PRINTF("GET challenge: %s\n", temp);
rest_set_header_content_type(response, TEXT_PLAIN);
rest_set_response_payload(response, temp, 64);

That's all there is to a basic GET handler.

The lock_resource is a little more complicated since it can handle GET, PUT, and POST. It also has to process query strings and post data. The declaration is similar to the challenge handler:

RESOURCE(lock, METHOD_GET | METHOD_POST | METHOD_PUT , "lock");
void lock_handler(REQUEST* request, RESPONSE* response)
{

This creates a lock resource that will accept METHOD_GET, or METHOD_POST, or METHOD_PUT.

In the handler code, we dispatch on the type of request received since we need to do different things for the different types of request:

switch(rest_get_method_type(request))
{
    case METHOD_GET:
           /* GET code */
           break;
    case METHOD_POST:
    case METHOD_PUT:
           /* PUT/POST code */
           break;
    default:
           break;
    }

For the GET code, we first get the "resp" query variable --- this is the necessary response.

            if (rest_get_query_variable(request, "resp", resp_str, 33)) {
                    resp_str[32] = 0;
                    PRINTF("got response: %s\n", resp_str);
                    hex_pack(resp, resp_str, 16);
            }

Then if the response is good, we return the state of the lock as either "locked" or "unlocked":

            if(check_response(resp)) {
                    if(lock_state == LOCKED) {
                            sprintf(temp,"locked");
                    } else {
                            sprintf(temp,"unlocked");
                    }
                    rest_set_header_content_type(response, TEXT_PLAIN);
                    rest_set_response_payload(response, temp, strlen(temp));
                    success = 1;
            }

Now for POST, we get the "resp" query variable in the same way, if it looks good then we set the state of the lock:

            if(check_response(resp)) {
                    if (rest_get_query_variable(request, "state", state, MAX_STATE_LEN)) {
                            PRINTF("PUT/POST to set lock state to: %s\n", state);
                            if (!strncmp(state,"locked",6)) {
                                    set_lock_state(LOCKED);
                                    success = 1;
                            } else if(!strncmp(state,"unlocked",8)) {
                                    set_lock_state(UNLOCKED);
                            success = 1;
                            }
                    }
            }

One final detail is that an unlocked lock will lock itself after a timeout. This is handled in the set_lock_state() routine:

/* time for lock to stay unlocked */
#define LOCK_TIMEOUT (10 * CLOCK_SECOND)
static struct ctimer lock_timer;

void set_lock_state(int state)
{
    switch(state)
    {
    case LOCKED:
            PRINTF("lock state set to LOCKED\n");
            leds_off(LEDS_ALL);
            leds_on(LEDS_RED);
            lock_state = LOCKED;
            break;
    case UNLOCKED:
            PRINTF("lock state set to UNLOCKED\n");
            leds_off(LEDS_ALL);
            leds_on(LEDS_GREEN);
            lock_state = UNLOCKED;
            ctimer_set(&lock_timer, LOCK_TIMEOUT, lock_timeout, NULL);
            break;
    }
}

A timeout is set when the lock is set to UNLOCKED using Contiki's ctimer. A ctimer schedules a function to be called after a certain amount of time. In this case, the function is lock_timeout and it's called LOCK_TIMEOUT time after the lock is UNLOCKED. lock_timeout is a simple function that prints a debug message and calls set_lock_state(LOCKED). In this way, a the lock re-locks itself after a certain about of time has passed.

Using the MC13224v Advanced Security Module (ASM) to create and check a 128-bit AES challenge

The 128-bit AES challenge and response is generated and checked using the AES hardware on the mc13224v, called the Advanced Security Module (ASM). The ASM is designed to be used to process secured 802.15.4 frames, which uses AES-CCM encryption. AES-CCM only requires the forward AES cipher and that is only what is available to use in the ASM.

AES-CCM is a combination of AES-CTR and AES-CBC-MAC. AES-CTR uses the forward cipher to encrypt a counter. That counter is then XORed with the plain text. AES-CBC-MAC uses the forward portion of the "cipher block chaining" (CBC) mode as a hashing function to sign data.

For the challenge/response here we are only using the AES-CTR fuctions of the ASM.

In fob-common.c: asm_init() initializes the ASM and then the secret key is loaded (of course, the real installation uses a different, more random key than the one showed below!):

    ASM->KEY0 = 0xccddeeff;
    ASM->KEY1 = 0x8899aabb;
    ASM->KEY2 = 0x44556677;
    ASM->KEY3 = 0x00112233;

and CTR mode is set:

    ASM->CONTROL1bits.CTR = 1;

The challenge is a random 32 bytes. The first 16 are considered the counter, and the second 16 are the data for the challenge.

The response to the challenge is generated as follows from coap-fob.c: make_response(). First the challenge data is loaded:

    ASM->CTR0 = *(uint32_t *)&challenge[0];
    ASM->CTR1 = *(uint32_t *)&challenge[4];
    ASM->CTR2 = *(uint32_t *)&challenge[8];
    ASM->CTR3 = *(uint32_t *)&challenge[12];

    ASM->DATA0 = *(uint32_t *)&challenge[16];
    ASM->DATA1 = *(uint32_t *)&challenge[20];
    ASM->DATA2 = *(uint32_t *)&challenge[24];
    ASM->DATA3 = *(uint32_t *)&challenge[28];

Then the ASM hardware is started and polled until the encryption completes:

    ASM->CONTROL0bits.START = 1;
    while(ASM->STATUSbits.DONE == 0) { continue; }

and the results are read out:

    resp[0] =  ASM->CTR0_RESULT       & 0xff;
    resp[1] =  ASM->CTR0_RESULT >> 8  & 0xff;
    resp[2] =  ASM->CTR0_RESULT >> 16 & 0xff;
    resp[3] =  ASM->CTR0_RESULT >> 24 & 0xff;

    resp[4] =  ASM->CTR1_RESULT       & 0xff;
    resp[5] =  ASM->CTR1_RESULT >> 8  & 0xff;
    resp[6] =  ASM->CTR1_RESULT >> 16 & 0xff;
    resp[7] =  ASM->CTR1_RESULT >> 24 & 0xff;

    resp[8] =  ASM->CTR2_RESULT       & 0xff;
    resp[9] =  ASM->CTR2_RESULT >> 8  & 0xff;
    resp[10] = ASM->CTR2_RESULT >> 16 & 0xff;
    resp[11] = ASM->CTR2_RESULT >> 24 & 0xff;

    resp[12] = ASM->CTR3_RESULT       & 0xff;
    resp[13] = ASM->CTR3_RESULT >> 8  & 0xff;
    resp[14] = ASM->CTR3_RESULT >> 16 & 0xff;
    resp[15] = ASM->CTR3_RESULT >> 24 & 0xff;

A similar process is used to verify the response to a challenge (see fob-server.c: check_response()).

Creating the key fob

The key fob is more complicated than the server as it handles user-input and displays status. The following is all from coap-fob.c.

The coap fob tracks two pieces of state: 1) whether is has a challenge from the fob-server and 2) what is is state of the lock. Additionally, pressing a button on the fob should set the lock state on the server to "unlocked".

The main coap-fob process does some initialization to get everything going, and then drops into its main event loop:

PROCESS_THREAD(coap_fob, ev, data)
{
  PROCESS_BEGIN();

  etimer_set(&et_poll_chal, 1 * CLOCK_SECOND);
  etimer_set(&et_lock_status, 5 * CLOCK_SECOND);
  have_challenge = 0;
  set_lock_status(STALE);
  while(1) {
    PROCESS_YIELD();
    /* dispatch code goes here */
    PROCESS_END();
}

We initialize the lock status to STALE and set have_challenge to zero. We also start two etimers: an etimer posts an event to all processes after a certain amount of time. We post a et_poll_chal event after 1 second to trigger a poll of the challenge and a et_lock_status event to get the state of the lock after 5 seconds.

The first dispatch handler deals with the challenge:

if((etimer_expired(&et_poll_chal) && !have_challenge) ||
   ev == ev_get_challenge) {
        PRINTF("get new challenge\n");
        if(etimer_expired(&et_poll_chal)) { PRINTF("timer expired\n"); }
        if(ev == ev_get_challenge) { PRINTF("get challenge event\n");}
        if(!have_challenge) {
                PRINTF("!have_challenge\n");
                last_getchal_id = send_get_challenge();
                etimer_set(&et_poll_chal, 1 * CLOCK_SECOND);
        }
}

If the et_poll_chal timer has expired and we don't have a challenge already OR if a special get_challenge event is posted, then as long as we don't have a challenge then we send a get challenge request and reset the poll timer. If the challenge request was successful, then the next time through, the timer will not get reset and the fob will stop asking for challenges.

The next handler deals with refreshing the lock status periodically:

if(etimer_expired(&et_lock_status) || ev == ev_get_lock_status) {
        PRINTF("refresh lock\n");
        set_lock_status(STALE);
        if(have_challenge) {
                last_get_lock_id = send_get_lock();
                process_post(&coap_fob, ev_get_challenge, NULL);
        } else {
                process_post(&coap_fob, ev_get_challenge, NULL);
                etimer_set(&et_lock_status, 1 * CLOCK_SECOND);
        }
}

If the lock status timer has expired OR we receive a lock_status event, then the lock status is reset to STALE. Next, if we have a challenge then we send a GET for the lock status, followed by posting the ev_get_challenge which will trigger the fob to get a new challenge so after. If we don't have a challenge, then we defer getting the lock status by reseting the timer to later and post a get_challenge event.

The next handler deals with the button press:

if (ev == sensors_event && data == &button_sensor) {
        PRINTF("Button pressed\n");
        set_lock_status(STALE);
        send_post_lock("unlocked");
        process_post(&coap_fob, ev_get_challenge, NULL);
        process_post(&coap_fob, ev_get_lock_status, NULL);
}

This one is rather straightforward. If the button is presses, set the lock state to STALE, post to the lock to switch to "unlocked" and then get a new challenge and get the new lock status.

Finally, there is one last catch-all handler:

if ((lock_status == STALE) && have_challenge) {
        process_post(&coap_fob, ev_get_lock_status, NULL);
}

This handles the case that whenever the lock_status is STALE and we have a challenge to use, then poll the lock status immediately.

Handling responses

Incoming data in handled in the dispatch loop from a tcpip_event:

if (ev == tcpip_event) {
        handle_incoming_data();
}

The data is checked, parsed, and then passed to the application's response_handler:

response_handler(coap_packet_t* response)
{
  uint16_t payload_len = 0;
  uint8_t* payload = NULL;
  payload_len = coap_get_payload(response, &payload);

  PRINTF("Response transaction id: %u", response->tid);
  if (payload) {
    memcpy(temp, payload, payload_len);
    temp[payload_len] = 0;
    PRINTF(" payload: %s\n", temp);
  }

This code recovers the payload of the response. We then dispatch on the transaction id (tid) to handle the payload data.

If the tid indicates this was a challenge request:

if(response->tid == last_getchal_id) {
        PRINTF("got new challenge\n ");
        have_challenge = 1;
        hex_pack(challenge, temp, 32);
        make_response();
}

then we mark that we now have a challenge; pack the result in to the challenge array, and prepare the response using the ASM.

And if the tid indicates that this was a get_lock status request:

if(response->tid == last_get_lock_id) {
        if (!strncmp(temp,"locked",6)) {
                set_lock_status(LOCKED);
        } else if(!strncmp(temp,"unlocked",8)) {
                set_lock_status(UNLOCKED);
        }
}

we set the lock status based on the payload accordingly.

Finally, set_lock_status handles changing the LEDs on the fob as well as rescheduling the timers.

Setting mac addresses and burning the firmware

The fob and server form point-to-point links (although RPL routing is enabled on them it isn't used for the application). The address of the server is hardcoded as:

#define SERVER_NODE(ipaddr)   uip_ip6addr(ipaddr, 0xfe80, 0, 0, 0, 0x0250, 0xc2a8, 0xca00, 0x0001)

That is:

fe80::0250:c2a8:ca00:0001

This is a link-local address and should not get routed more than one-hop.

The address of a node is derived from it MAC address, which on the Econotag platform, is stored at the non-volatile address 0x1e000.

In cpu/mc1322x/tools there are a few tools to help in setting mac addresses. The first one is burn-mac.pl which stores only the mac address into NVM. You can use this to set the MAC address for nodes that you are developing code on (i.e. loading programs only into RAM for testing).

For this example there are two wrapper scripts: burn-mac-fob.sh and burn-mac-server.sh. The contents of burn-mac-server.sh are:

#!/bin/sh

../../cpu/mc1322x/tools/burn-mac.pl --iab=a8c ~/libmc1322x/tests/flasher_redbee-econotag.bin a000001

If you run this first on a node, then when you load your test code you will have the mac address set.

To create binaries that will have the MAC address set, you can use bin2macbin.pl. For this example, the wrapper script is mac-bins.sh which contains:

#!/bin/sh

../../cpu/mc1322x/tools/bin2macbin.pl --iab=a8c a000001 fob-server_redbee-econotag.bin
../../cpu/mc1322x/tools/bin2macbin.pl --iab=a8c a000002 coap-fob_redbee-econotag.bin

Running this will create fob-server_redbee-econotag-0050c2a8ca000001.bin and coap-fob_redbee-econotag-0050c2a8ca000002.bin. Programming these binaries on to an mc13224v will also set the MAC addresses accordingly.