CoMatrix: OSCORE

From Embedded Lab Vienna for IoT & Security
Jump to navigation Jump to search

Summary

This documentation describes the usage of an application-layer protocol OSCORE to provide end-to-end protection between the CoMatrix Gateway and CoMatrix Client.


Requirements

  • Operating system:
    • Ubuntu 22.04 (Gateway)
    • RIOT OS (Client)
  • See #Used Hardware

In order to complete these steps, you must set up the CoMatrix project before.

Description

OSCORE is a security protocol designed to provide end-to-end protection between endpoints communicating using CoAP.

The keys are pre-shared between the client and the gateway because there is no key exchange protocol in OSCORE itself. However, there is ongoing work with key exchange protocols such as EDHOC or ACE-OSCORE

Gateway

The Python library aiocoap provides the OSCORE functionality and was used in order for the gateway to understand OSCORE within CoAP.

Imports

The following imports were added

from aiocoap.oscore_sitewrapper import OscoreSiteWrapper
from aiocoap.credentials import CredentialsMap
from plugtest_common import *
from aiocoap.cli.common import server_context_from_arguments, add_server_arguments
  • The plugtest_common import was extracted from the aiocoap OSCORE plugtest and provides verification for external AADs and returning the security context.

Main

When starting the gateway, new parameters have been added to list the directory where the pre-shared secrets are held and sequence numbers will be saved. These sequence numbers are necessary for replay protection.

parser.add_argument("contextdir", help="Directory name where to persist sequence numbers and the location of the secrets", type=Path)

// Necessary to add these arguments provided by the aiocoap library in order for the server to start correctly. More info under aiocoap/cli/common.py
add_server_arguments(parser)


The context directory may contain two files:

  • secrets.json
  • settings.json

As per implementation it is sufficient to only have one of these files in the directory.

The format of the file is the following JSON:

{
  "sender-id_hex": "01",
  "recipient-id_ascii": "file",
  "secret_ascii": "Correct Horse Battery Staple"
}

Important properties are:

  • sender-id
  • recipient-id
  • secret

These three properties must be appended with either _hex or _ascii and have the values according to the suffix.

Assume that the file above was for the server and for our client we would need the following file:

{
  "sender-id_ascii": "file",
  "recipient-id_hex": "01",
  "secret_ascii": "Correct Horse Battery Staple"
}

Notice how sender-id and recipient-id had to be swapped. With these two secret files it is now possible to start a gateway and a client each using a different JSON file with the ids flipped.

For further information see aiocoap-OSCORE


Loading these in the code was done in the following way - For testing purposes there were two pairs of credentials a / b and c / d whereas the first one is used for the test client and the latter for the gateway:

// Loading the pre-shared secrets
server_credentials = CredentialsMap()
// the first parameter states the name of the context in this case it's b but can be named in any way and the second parameter is the path to the folder e.g. the file would be located in /contextdir/b/settings.json
server_credentials[':b'] = get_security_context('b', args.contextdir / "b")
server_credentials[':d'] = get_security_context('d', args.contextdir / "d")

To enable OSCORE for the server now the following code was added/adjusted:

// root is the variable for the site
// Enable the site to talK OSCORE with the credentials loaded from before
root = OscoreSiteWrapper(root, server_credentials)
args.bind = bind
// Now we create a server context with arguments added
asyncio.Task(server_context_from_arguments(root, args))

USAGE: ./comatrix_gateway.py contextdir

CoMatrix-Gateway-OSCORE.png

To test if the gateway actually talks in OSCORE the plugtest-client from the aiocoap library was adjusted to meet our needs for comatrix.

USAGE: ./unit-tests.py host contextdir

Unittest-OSCORE.png

Wireshark Capture

The Wireshark Capture can be downloaded here: Media:OSCORE-wireshark-capture.zip

OSCORE-wireshark.png

Client

The Comatrix project already uses RIOT-OS for the client board so we continued using it. For the implementation of OSCORE on RIOT-OS we tried using liboscore. It comes with it's own build stack and is therefore different from the instructions provided by the Comatrix project.


Setup the build stack

First, the liboscore project has to be cloned locally with the recursive parameter so that it contains RIOT-OS itself. This can be achieved by following the steps provided in Common set-up instructions for RIOT-based demos The folders 'comatrix', 'example_comatrix_chat' and 'example_comatrix_tempsensor' from the Comatrix client directory then have to be copied into the folder 'liboscore/tests/riot-tests/'. Because the RIOT-OS code is now in a different location in relation to the Makefile, it has to be adapted.

Remove this line:

include $(RIOTBASE)/Makefile.include

Add this line:

include ../Makefile.include

The project can then be built by using

make all

Adopt liboscore

The comatrix examples for the chat and the tempsensor both use the source code in the comatrix folder which handles the CoAP messaging. This is why we did not change the examples but rather the comatrix code to implement OSCORE centrally. Because the liboscore is still in a very early state and wasn't maintained for about two years we rarely found working examples so the focus was set onto the file 'demo-server.c' within the test 'plugtest-server' within the riot-tests folder which is documented as Demo: Running the OSCORE plug test server on RIOT native . This code makes use of threading and mutex because it also acts as a server which we don't need because the chat and tempsensor examples only act as clients. We have to work around those parts and only use the functions for preparing and encrypting the OSCORE message.

First we added the includes to oscore

#include <oscore_native/message.h>
#include <oscore/contextpair.h>
#include <oscore/context_impl/primitive.h>
#include <oscore/context_impl/b1.h>
#include <oscore/protection.h>

Then, to generate the security context, we need to call the script 'oscore-key-derivation' as documented in Demo: Peer-to-peer exchanges in 6LoWPAN network, but we used the parameter '--format header' to retrieve the context in a matter that the compiler understands. This block is then copied to the comatrix.c file. An example of this is:

#define SENDER_KEY {255, 177, 78, 9, 60, 148, 201, 202, 201, 71, 22, 72, 180, 249, 135, 16}
#define RECIPIENT_KEY {240, 145, 14, 215, 41, 94, 106, 212, 181, 79, 199, 147, 21, 67, 2, 255}
#define COMMON_IV {70, 34, 212, 221, 109, 148, 65, 104, 238, 251, 84, 152, 124}
#define SENDER_ID {1}
#define RECIPIENT_ID {}

The security context then has to be created

static struct oscore_context_primitive_immutables immutables_d = {
    .common_iv = COMMON_IV,
    
    .recipient_id_len = 0,
    .recipient_key = RECIPIENT_KEY,
    
    .sender_id_len = 1,
    .sender_id = "\x01",
    .sender_key = SENDER_KEY,
};
static struct oscore_context_primitive primitive_d = { .immutables = &immutables_d };
oscore_context_t secctx_d = {
    .type = OSCORE_CONTEXT_PRIMITIVE,
    .data = (void*)(&primitive_d),
};
int16_t secctx_d_change = 0;

As the first function to adapt we decided to use the function comatrix_sendmsg as we can set different text at runtime in the CLI and immediately get the results while capturing with Wireshark. The following code is not working at the moment as we are facing issues with the method 'oscore_encrypt_message'. While debugging it we found out that the payload gets a special character somewhere during the preparation so that the length of the ciphertext cannot be calculated correctly.

/**
 *  @brief sends a COAP send message PUT request to the gateway, type is non-confirmable and no response handler is called
 *
 *  Send message request is non-confirmable for very constraint devices like sensor nodes, when communication errors occur the packet will not be resent
 *
 *  @param[in] msgbuf               message buffer.
 *  @param[in] msglen               length of message buffer.
 *  @param[in] callback_handler     should be 0; not implemented yet because it is non-confirmable
 *
 *  @return length of the payload
 *  @return 0   when the message exceeds PDU payload buffer size
 *  @return -1  when CoAP send message PUT request failed
 **/
int comatrix_sendmsg(char *msgbuf, size_t msglen, comatrix_callback_t callback_handler) {
   (void)callback_handler;   /* not used by now because CoAP type is non-confirmable */
   coap_pkt_t pdu;
   uint8_t    pdu_buf[CONFIG_GCOAP_PDU_BUF_SIZE];
   size_t     len       = 0;
   char       separator = '\0';
   oscore_msg_protected_t oscmsg;

   struct static_request_data request_data = { .done = MUTEX_INIT_LOCKED };

   /* build proxy string */
  #ifdef CONFIG_COMATRIX_ENABLE_SHORTURL
   int  proxy_buf_size = 20 + strlen(CONFIG_COMATRIX_SYNAPSE) + strlen(_cstate.comatrix_roomid) + snprintf(NULL, 0, "%d", _cstate.tx_id);
   char proxy_buf[proxy_buf_size];
   int  le = snprintf(proxy_buf, proxy_buf_size, "%s/9/%s/m.room.message/%d", CONFIG_COMATRIX_SYNAPSE, _cstate.comatrix_roomid, _cstate.tx_id);
  #else
   int  proxy_buf_size = 49 + strlen(CONFIG_COMATRIX_SYNAPSE) + strlen(_cstate.comatrix_roomid) + snprintf(NULL, 0, "%d", _cstate.tx_id);
   char proxy_buf[proxy_buf_size];
   int  le = snprintf(proxy_buf, proxy_buf_size, "%s/_matrix/client/r0/rooms/%s/send/m.room.message/%d", CONFIG_COMATRIX_SYNAPSE, _cstate.comatrix_roomid, _cstate.tx_id);
  #endif
   DEBUG("[comatrix_sendmsg:] proxystring len: %d \n", le);

   /* increment message counter */
   _cstate.tx_id = _cstate.tx_id + 1;

   /* initialize coap packet and set header and options */
   gcoap_req_init(&pdu, pdu_buf, CONFIG_GCOAP_PDU_BUF_SIZE, COAP_METHOD_PUT, COMATRIX_SENDMSG_PATH);
   coap_hdr_set_type(pdu.hdr, COAP_TYPE_NON);
   coap_opt_add_format(&pdu, COAP_FORMAT_CBOR);
   coap_opt_add_proxy_uri(&pdu, proxy_buf);
   coap_opt_add_chars(&pdu, COMATRIX_OPT_NUM, _cstate.comatrix_token, strlen(_cstate.comatrix_token), separator);
   len = coap_opt_finish(&pdu, COAP_OPT_FINISH_PAYLOAD);

   oscore_msg_native_t native = { .pkt = &pdu };
   secctx_d_change += 1;

   if (oscore_prepare_request(native, &oscmsg, &secctx_d, &request_data.request_id) != OSCORE_PREPARE_OK) {
      printf("Failed to prepare request encryption\n");
      return(0);
   }

   oscore_msg_protected_set_code(&oscmsg, 0x02 /* POST */);

   /* validate input length and encode payload to cbor {"msgtype":"m.text","body":""} - 22 bytes cbor without msg */
   if (pdu.payload_len >= msglen + 23) {
      size_t cborlen = encbor_comatrixmsg((uint8_t *)pdu.payload, msgbuf, msglen);;
      len += cborlen;
   }
   else{
      DEBUG("[comatrix_sendmsg:] ERROR. The message buffer is too small for the message\n");
      return(0);
   }

   oscore_msgerr_protected_t oscerr;
   oscerr = oscore_msg_protected_append_option(&oscmsg, 11 /* Uri-Path */, (uint8_t*)"uripath", 7);
   if (oscore_msgerr_protected_is_error(oscerr)) {
      printf("Failed to add option\n");
      goto error;
   }


   uint8_t *payload;
   size_t payload_length;
   oscerr = oscore_msg_protected_map_payload(&oscmsg, &payload, &payload_length);
   if (oscore_msgerr_protected_is_error(oscerr)) {
      printf("Failed to map payload\n");
      goto error;
   }
   printf("map_payload: %s", payload);

   /*payload = (uint8_t*)malloc(100 * sizeof(*payload));
   memcpy(payload, msgbuf, len);
   printf("new payload: %s", payload);*/

   oscerr = oscore_msg_protected_trim_payload(&oscmsg, 1);
   if (oscore_msgerr_protected_is_error(oscerr)) {
      printf("Failed to truncate payload\n");
      goto error;
   }

   oscore_msg_native_t pdu_write_out;
   if (oscore_encrypt_message(&oscmsg, &pdu_write_out) != OSCORE_FINISH_OK) {
      // see FIXME in oscore_encrypt_message description
      printf("Failed to encrypt payload\n");
      goto error;
   }

   DEBUG("[comatrix_sendmsg:] sending msg ID %u, %u bytes\n", coap_get_id(&pdu), (unsigned)len);
   if (!gcoap_req_send(pdu_buf, len, &_cstate.remote, NULL, &request_data)) {
      DEBUG("[comatrix_sendmsg:] msg send failed\n");
      return(-1);
   }

error:
    {}
    // FIXME: abort encryption (but no PDU recovery and PDU freeing necessary on this backend as it's all stack allocated)

   return(len);
}

Current status

As we are facing issues regarding the liboscore code that lacks maintenance and documentation we have to deep dive into the functions. After these issues are resolved the remaining implementation is assumed to be straight forward for the other comatrix_* functions.

Used Hardware

References