Secure Resource Sharing for Embedded Protected Module Architectures

Jo Van Bulck, Job Noorman, Jan Tobias Mühlberg and Frank Piessens

This page provides the source code and usage examples of the protected file system, presented at the 9th WISTP International Conference on Information Security Theory and Practice (WISTP'15).

Below, we repeat the abstract of the paper and provide an example program to demonstrate our protected file system API. Finally, we clarify the implementation of the access control layer by means of short code snippets.

Abstract

Low-end embedded devices and the Internet of Things (IoT) are becoming increasingly important for our lives. They are being used in domains such as infrastructure management, and medical and healthcare systems, where business interests and our security and privacy are at stake. Yet, security mechanisms have been appallingly neglected on many IoT platforms. In this paper we present a secure access control mechanism for extremely lightweight embedded microcontrollers. Being based on Sancus, a hardware-only Trusted Computing Base and Protected Module Architecture for the embedded domain, our mechanism allows for multiple software modules on an IoT-node to securely share resources. We implement and evaluate our approach for two application scenarios, a shared memory system and a shared flash drive. Our implementation is based on a Sancus-enabled TI MSP430 microcontroller. We show that our mechanism can give high security guarantees at small runtime overheads and a moderately increased size of the Trusted Computing Base.

Example Client Program

In this section we provide a minimal example that uses our SFS file system API. The program simple-test.c listed below defines two Sancus modules, named clientA and clientB. ClientA first creates a new empty file and automatically becomes the "owner" of this file. ClientA thereafter writes out some data bytes and assigns read-only permissions for clientB. Next, control is transferred to clientB, which successfully opens the file, reads the data and returns. ClientA then revokes the access permissions for clientB on the logical file and transfers control to clientB once more. Of course, this time access should be denied. Finally, clientA closes and removes the logical file.


#include "../common.h"          // utility functions
#include <sancus/sm_support.h>  // Sancus support
#include "../sfs/sfs.h"         // Sancus File System

#define THE_FILENAME        'a'
#define THE_INIT_FILESIZE   100
#define THE_TEST_DATA       "Lorem ipsum dolor sit amet, consectetur adipiscing elit."

DECLARE_SM(clientB, 0x1234);
#define B   "[clientB] "

void SM_ENTRY("clientB") run_clientB(void)
{
    printf_int_int(B "Hi, I have id %d and was called from %d\n",
        sancus_get_self_id(), sancus_get_caller_id());

    int fd, i = 0; char cur;
    
    puts(B "attempting to open the file and read the data");
    fd = sfs_open(THE_FILENAME, SFS_READ, SFS_OPEN_EXISTING);
    if (fd < 0)
    {
        puts(B "access to the file has been denied");
        return;
    }

    printf_str(B "the chars are: \"");
    while((cur = sfs_getc(fd)) != EOF)
        printf_int("%c", cur);
    printf_str("\"\n");
    
    sfs_close(fd);
}

DECLARE_SM(clientA, 0x1234);
#define A   "[clientA] "

void SM_ENTRY("clientA") run_clientA(void)
{
    printf_int_int(A "Hi, I have id %d and was called from %d\n",
        sancus_get_self_id(), sancus_get_caller_id());
        
    int fd, i; char *str = THE_TEST_DATA;
    sm_id idB = sancus_get_id(clientB.public_start);

    puts(A "creating the file and writing the data");
    fd = sfs_open(THE_FILENAME, SFS_CREATOR, THE_INIT_FILESIZE);
    for(i = 0; str[i]; i++)
        sfs_putc(fd, str[i]);
    
    puts(A "assigning read-only permissions for clientB and dumping ACLs");
    sfs_chmod(THE_FILENAME, idB, SFS_READ);
    sfs_dump();
    
    run_clientB();

    puts(A "revoking permissions for clientB and dumping ACLs");    
    sfs_chmod(THE_FILENAME, idB, SFS_NIL);
    sfs_dump();
 
    run_clientB();
    
    puts(A "closing and removing the logical file");
    sfs_close(fd);
    sfs_remove(THE_FILENAME);
}

int main()
{
    WDTCTL = WDTPW | WDTHOLD;
    uart_init();
    puts("\n---------------\nmain started");
    
    sancus_enable(&sfs);
    sancus_enable(&clientA);
    sancus_enable(&clientB);    

    run_clientA();
    
    puts("[main] exiting\n-----------------");
    while(1) {}
}

To compile and run the above example, one should first install Sancus. The following Makefile builds the test program and uploads it to the FPGA. The desired CFS back-end can be chosen at compile time through the SFS_BACKEND constant. The SHM back-end operates on a Sancus-protected memory buffer and thus realises a form of protected shared memory. The COFFEE back-end corresponds to Contiki's Coffee file system, optimised for embedded flash drives.


SANCUS_SUPPORT_DIR = /usr/local/share/sancus-support
DEVICE             = /dev/ttyUSB0
VENDOR_KEY         = 4078d505d82099ba

CC                 = sancus-cc
LD                 = sancus-ld
CRYPTO             = sancus-crypto
LOAD               = sancus-loader
RM                 = rm -f

DEBUG_LEVEL        = -DSFS_WARNING #-DNODEBUG #-DNOCOLOR #-DCOFFEE_DEBUG -DFLASH_DEBUG
CFS_FORMAT         = -DNO_CFS_FORMAT # comment to format the flash drive on boot

CFLAGS             = -I$(SANCUS_SUPPORT_DIR)/include/ -g --verbose -Wfatal-errors $(DEBUG_LEVEL) $(CFS_FORMAT)
LDFLAGS            = --standalone --verbose --ram-size 24K --rom-size 32K
LIBS               = -L$(SANCUS_SUPPORT_DIR)/lib -ldev-uart -ldev-spi
CRYPTOFLAGS        = --key $(VENDOR_KEY)
LOADFLAGS          = -device $(DEVICE) -baudrate 115200

COFFEE             = ../sfs/cfs/cfs-coffee.o
SHM                = ../sfs/shm/shared-mem.o ../sfs/shm/my_malloc.o
SFS_BACKEND        = $(COFFEE) #$(SHM)
SFS                = ../sfs/sfs-ram.o $(SFS_BACKEND)

OBJECTS            = simple-test.o ../common.o $(SFS)
TARGET             = sfs-test.elf
TARGET_NO_MACS     = $(TARGET)-no-macs

$(TARGET): $(TARGET_NO_MACS)
	$(CRYPTO) $(CRYPTOFLAGS) -o $@ $<

$(TARGET_NO_MACS): $(OBJECTS)
	$(LD) $(LDFLAGS) -o $@ $^ $(LIBS)

.PHONY: load
load: $(TARGET)
	$(LOAD) $(LOADFLAGS) $<

.PHONY: clean
clean:
	$(RM) $(TARGET) $(TARGET_NO_MACS) $(OBJECTS)

The output of the test program looks as follows:

output of the test program

Access Control Layer Implementation

In this section we illustrate the implementation of the generic front-end access control layer by means of short code snippets. The front-end is conceived as a wrapper implementation that associates an Access Control List (ACL) of (smID, permission_flags) pairs per logical file to validate the caller's permissions before passing the call to the back-end. Logical files are represented by a linked list of OPEN_FILE structs that each hold the file name and a linked list of FILE_PERM structs that make up the ACL. Once a file has been successfully opened, the corresponding FILE_PERM can be looked up quickly via a file-descriptor indexed array fd_cache.

Access control is ensured through the two macros listed below. The CHK_FD macro checks whether a specified file descriptor is within bounds and belongs to a specified SM, whereas the CHK_PERM macro can be used to verify bitmask-permissions.


#define CHK_FD(fd, sm) \
    if ((fd < 0 || fd >= MAX_NB_OPEN_FILES) || !fd_cache[fd] || fd_cache[fd]->sm_id != sm) \
    { \
        printerror_int_int("the provided file descriptor %d isn't valid or " \
            "doesn't belong to calling SM %d", fd, sm); \
        return FAILURE; \
    }

#define CHK_PERM(p_have, p_want) \
do { \
    if ((p_have & p_want) != p_want) { \
        printerror_int_int("permission check failed p_have=0x%x ; p_want = 0x%x", \
            p_have, p_want); \
        return FAILURE; \
    } \
} while(0)

The following code snippet defines the sfs_getc() implementation that uses the sancus_get_caller_id() hardware instruction and the above macros to enforce SM-grained logical file access control. The function returns FAILURE when access is denied and otherwise returns the result of the cfs_read() back-end call.


int SM_ENTRY("sfs") sfs_getc(int fd)
{
    sm_id caller_id = sancus_get_caller_id();
   
    CHK_FD(fd, caller_id)
    CHK_PERM(fd_cache[fd]->flags, SFS_READ);
 
    return (cfs_read(fd, &buf, 1) > 0)? buf : EOF;
}

The following code snippet defines the implementation of the sfs_chmod function, which can be used to manipulate the SM-grained ACL of a logical file. As evident from the implementation below, only calling SMs that hold the special SFS_ROOT permission are allowed to change the associated ACL. The creator/owner of a logical file always holds this permission. The access rights for the caller are looked up in two phases: the OPEN_FILE struct for the given file name is first located and the associated ACL is thereafter traversed to locate the according FILE_PERM struct. The code snippet below furthermore shows how the special SFS_NIL permission is used to revoke existing access rights.


int SM_ENTRY("sfs") sfs_chmod(filename_t name, sm_id id, int perm_flags)
{
    sm_id caller_id = sancus_get_caller_id();

    // only root can chmod
    struct FILE_PERM *p_caller;
    if (two_phase_lookup(name, SFS_ROOT, caller_id, &p_caller) < 0)
        return FAILURE;
    
    if (perm_flags == SFS_NIL)
        return revoke_acl(p_caller->file, id);
   
    return add_acl(p_caller->file, id, perm_flags);
}