Secure on Secure.
Ada sets the gold standard for developing safety-critical applications. However, the complex requirements of modern embedded systems are increasingly met with the integration of readily available 3rd party libraries – typically not written in Ada. MultiZone™ Security provides a robust and cost-effective mechanism to isolate untrusted 3rd party software, making it a perfect complement to Ada components.
C is the dominant language of the embedded world, almost to the point of exclusivity. Due to its age, and its goal of being a “portable assembler”, it deliberately lacks type-safety that languages like Ada provide. The lack of type-safety in C is one of the reasons for the commonness of embedded device exploits. Proposed solutions are partitioning the application into smaller intercommunicating blocks, designed with the principle of least privilege in mind; and rewriting the application in a type-safe language. We believe that both approaches are complementary and want to show you how to combine separation and isolation provided by MultiZone together with iteratively rewriting parts in Ada. We will take the MultiZone SDK demo and rewrite one of the zones in Ada. The full demo simulates an industrial application with a robotic arm. It runs on the Arty A7-35T board and interfaces with the PC and a robotic arm (OWI-535 Robotic Arm) via an SPI to USB converter. More details are available from the MultiZone Security SDK for Ada manual (https://github.com/hex-five/multizone-ada/blob/master/manual.pdf). We will be focusing on the porting process here.
MultiZone(TM) Security is the first Trusted Execution Environment for RISC-V – it enables development of a simple, policy-based security environment for RISC-V that supports rich operating systems through to bare metal code. It is a culmination of the embedded security best practices developed over the last decade and now applied to RISC-V processors. Instead of splitting into the secure and non-secure domain, MultiZoneTM Security provides policy-based hardware-enforced separation for an unlimited number of security domains, with full control over data, code and peripherals.
MultiZoneTM Security consists of the following components:
- MultiZone(TM) nanoKernel – lightweight, formally verifiable, bare metal kernel providing policy-driven hardware-enforced separation of ram, rom, i/o and interrupts.
- InterZone(TM) Messenger – communications infrastructure to exchange secure messages across zones on a no- shared memory basis.
- MultiZone(TM) Configurator – combines fully linked zone executables with policies and kernel to generate the signed firmware image.
- MultiZone(TM) Signed Boot – 2-stage signed boot loader to verify integrity and authenticity of the firmware image (sha-256 / ECC)
Contrary to traditional solutions, MultiZone(TM) Security requires no additional hardware, dedicated cores or clunky programming models. Open source libraries, third party binaries and legacy code can be configured in minutes to achieve unprecedented levels of safety and security. See https://hex-five.com/ for more details or check out the MultiZone SDK repository on GitHub – https://github.com/hex-five/multizone-sdk.
Ada on MultiZone
We port zone 3, the zone controlling the robotic arm, to Ada. The zone communicates with other zones via MultiZone APIs and with the robotic arm by bitbanging GPIO pins.
MultiZone zones differ from a bare metal applications as access to resources is restricted – a zone has only a portion of the RAM and FLASH and can only access some of the peripherals. Looking at our configuration (https://github.com/hex-five/multizone-ada/blob/master/bsp/X300/multizone.cfg), zone3 has the following access privileges:
Zone = 3 # base = 0x20430000; size = 64K; rwx = rx # FLASH base = 0x80003000; size = 4K; rwx = rw # RAM base = 0x0200BFF8; size = 0x8; rwx = r # RTC base = 0x10012000; size = 0x100; rwx = rw # GPIO
In the Ada world, this translates to having a separate runtime that we need to create. Luckily, AdaCore has released sources to their existing runtimes on GitHub – https://github.com/adacore/bb-runtimes and they have also included a how-to for creating new runtimes – https://github.com/AdaCore/bb-runtimes/tree/community-2018/doc/porting_runtime_for_cortex_m. Here’s how we create our customized HiFive1 runtime:
./build_rts.py --bsps-only --output=build --prefix=lib/gnat hifive1
This creates sources for building a runtime using a mix of sources from bb-runtimes and from the compiler itself thanks to the –bsps-only flag. Without this switch, we would need the original GNAT repository, which is not publicly available. Notice we don’t use –link, so our runtime sources are a proper copy and can be checked into a new git repo – https://github.com/hex-five/multizone-ada/tree/master/bsp/X300/runtime. Our runtime needs to be compiled and installed before it can be used, and we do that automatically as part of the Makefile for zone3 – https://github.com/hex-five/multizone-ada/blob/master/zone3/Makefile:
BSP_BASE := ../bsp PLATFORM_DIR := $(BSP_BASE)/$(BOARD) RUNTIME_DIR := $(PLATFORM_DIR)/runtime GPRBUILD := $(abspath $(GNAT))/bin/gprbuild GPRINSTALL := $(abspath $(GNAT))/bin/gprinstall .PHONY: all all: $(GPRBUILD) -p -P $(RUNTIME_DIR)/zfp_hifive1.gpr $(GPRINSTALL) -f -p -P $(RUNTIME_DIR)/zfp_hifive1.gpr --prefix=$(RUNTIME_DIR) $(AR) cr $(RUNTIME_DIR)/lib/gnat/zfp-hifive1/adalib/libgnat.a $(RUNTIME_DIR)/hifive1/zfp/obj/*.o $(GPRBUILD) -f -p -P zone3.gpr $(OBJCOPY) -O ihex obj/main zone3.hex --gap-fill 0x00
Note that we use a combination of GPRbuild and Make to minimize code differences between the two repositories as much as possible. GPRbuild is limited to zone3 only.
We use the Ada Drivers Library on GitHub (https://github.com/AdaCore/Ada_Drivers_Library) as a starting point as it provides examples for a variety of boards and architectures. Our target is the X300, itself a modified HiFive1/FE310/FE300. The differences between FE310/FE300 and X300 are detailed on the multizone-fpga GitHub repository (https://github.com/hex-five/multizone-fpga).
We need to change the drivers for our application slightly – we want LD0 for indicating the status of the robotic arm, red blink when disconnected and green when connected:
with FE310.Device; use FE310.Device; with SiFive.GPIO; use SiFive.GPIO; package Board.LEDs is subtype User_LED is GPIO_Point; Red_LED : User_LED renames P01; Green_LED : User_LED renames P02; Blue_LED : User_LED renames P03; procedure Initialize; -- MUST be called prior to any use of the LEDs procedure Turn_On (This : in out User_LED) renames SiFive.GPIO.Set; procedure Turn_Off (This : in out User_LED) renames SiFive.GPIO.Clear; procedure Toggle (This : in out User_LED) renames SiFive.GPIO.Toggle; procedure All_LEDs_Off with Inline; procedure All_LEDs_On with Inline; end Board.LEDs;
Having the package named Board allows us to modify the target board at compile-time by just providing the folder containing the Board package. This goes in line with what multizone-sdk does.
MultiZone nanoKernel offers trap and emulate so existing applications can be provided to MultiZone directly unmodified and will work as expected. Access to RAM, FLASH and peripherals needs to be allowed in the configuration file, though. MultiZone does provide functionality for increased performance, better power usage and interzone communication via the API in Libhexfive. Here we create an Ada wrapper around libhexfive32.a, providing the MultiZone specific calls:
package MultiZone is procedure ECALL_YIELD; -- libhexfive.h:8 pragma Import (C, ECALL_YIELD, "ECALL_YIELD"); procedure ECALL_WFI; -- libhexfive.h:9 pragma Import (C, ECALL_WFI, "ECALL_WFI"); ... end MultiZone;
A typical MultiZone optimized application will yield whenever it doesn’t have anything to do, to save on processing time and power:
with MultiZone; use MultiZone; procedure Main is begin -- Application initialization loop -- Application code ECALL_YIELD; end loop; end Main;
If a zone wants to communicate with other zones, such as receiving commands and sending back replies, it needs to use the ECALL_SEND/ECALL_RECV. These send/receive a chunk of 16 bytes and return a status whether the send/receive was successful. The prototypes are a bit special, as they take a void * parameter, which translates to System.Address:
function ECALL_SEND_C (arg1 : int; arg2 : System.Address) return int; -- libhexfive.h:11 pragma Import (C, ECALL_SEND_C, "ECALL_SEND"); function ECALL_RECV_C (arg1 : int; arg2 : System.Address) return int; -- libhexfive.h:12 pragma Import (C, ECALL_RECV_C, "ECALL_RECV");
We wrap these to provide a more Ada idiomatic alternative:
type Word is new Unsigned_32; type Message is array (0 .. 3) of aliased Word; pragma Pack (Message); subtype Zone is int range 1 .. int'Last; function Ecall_Send (to : Zone; msg : Message) return Boolean; function Ecall_Recv (from : Zone; msg : out Message) return Boolean;
We hide the System.Address usage and provide a safer subtype for the source/destination zone, since zone cannot be negative or 0.
function Ecall_Send (to : Zone; msg : Message) return Boolean is begin return ECALL_SEND_C (to, msg'Address) = 1; end Ecall_Send; function Ecall_Recv (from : Zone; msg : out Message) return Boolean is begin return ECALL_RECV_C (from, msg'Address) = 1; end Ecall_Recv;
With all the primitives in place, we can make a simple MultiZone optimized application that can respond to a ping from another zone:
with HAL; use HAL; with MultiZone; use MultiZone; procedure Main is begin -- Application initialization loop -- Application code declare msg : Message; Status : Boolean := Ecall_Recv (1, msg); begin if Status then if msg(0) = Character'Pos('p') and msg(1) = Character'Pos('i') and msg(2) = Character'Pos('n') and msg(3) = Character'Pos('g') then Status := Ecall_Send (1, msg); end if; end if; end; ECALL_YIELD; end loop; end Main;
Keeping some legacy (OWI Robot)
We keep the SPI functionality and the Owi Task as C files and just create Ada bindings for them. spi_c.c implements the SPI protocol by bit-banging GPIO pins:
package Spi is procedure spi_init; -- ./spi.h:8 pragma Import (C, spi_init, "spi_init"); function spi_rw (cmd : System.Address) return UInt32; -- ./spi.h:9 pragma Import (C, spi_rw, "spi_rw"); end Spi;
The OwiTask (owi_task.c) is a state machine containing different robot sequences. The main function is owi_task_run, while others are change the active state. owi_task_run returns the next SPI command to send via SPI for a given moment in time:
package OwiTask is procedure owi_task_start_request; -- ./owi_task.h:8 pragma Import (C, owi_task_start_request, "owi_task_start_request"); procedure owi_task_stop_request; -- ./owi_task.h:9 pragma Import (C, owi_task_stop_request, "owi_task_stop_request"); procedure owi_task_fold; -- ./owi_task.h:10 pragma Import (C, owi_task_fold, "owi_task_fold"); procedure owi_task_unfold; -- ./owi_task.h:11 pragma Import (C, owi_task_unfold, "owi_task_unfold"); function owi_task_run (time : UInt64) return UInt32; -- ./owi_task.h:12 pragma Import (C, owi_task_run, "owi_task_run"); end OwiTask;
The following Ada code runs the state machine for the OWI robot. Since the target functions are written in C, we see how Ada can interact with legacy C code:
-- OWI sequence run if usb_state = 16#12670000# then declare cmd_word : UInt32 := owi_task_run(CLINT.Machine_Time); cmd_bytes : Cmd; begin if cmd_word /= -1 then cmd_bytes(0) := UInt8 (cmd_word and 16#FF#); cmd_bytes(1) := UInt8 (Shift_Right (cmd_word, 8) and 16#FF#); cmd_bytes(2) := UInt8 (Shift_Right (cmd_word, 16) and 16#FF#); rx_data := spi_rw (cmd_bytes'Address); ping_timer := CLINT.Machine_Time + PING_TIME; end if; end; end if;
We now port over the owi sequence selection:
declare Status : Boolean := Ecall_Recv (1, msg); begin if Status then -- OWI sequence select if usb_state = 16#12670000# then case msg(0) is when Character'Pos('<') => owi_task_fold; when Character'Pos('>') => owi_task_unfold; when Character'Pos('1') => owi_task_start_request; when Character'Pos('0') => owi_task_stop_request; when others => null; end case; end if; ...
We leave it as an exercise for the reader to port the OWI sequence handler code to Ada.
We want the LED color to change as a result of the robot connected or disconnected events. Each of the LED colors is a separate pin on the board:
Red_LED : User_LED renames P01; Green_LED : User_LED renames P02; Blue_LED : User_LED renames P03;
One way of handling this is to create an Access Type that can store the currently selected LED color:
procedure Main is type User_LED_Select is access all User_LED; ... LED : User_LED_Select := Red_LED'Access; ... begin ... -- Detect USB state every 1sec if CLINT.Machine_Time > ping_timer then rx_data := spi_rw(CMD_DUMMY'Address); ping_timer := CLINT.Machine_Time + PING_TIME; end if; -- Update USB state declare Status : int; begin if rx_data /= usb_state then usb_state := rx_data; if rx_data = UInt32'(16#12670000#) then LED := Green_LED'Access; else LED := Red_LED'Access; owi_task_stop_request; end if; end if; end;
The actual blinking is then just a matter of dereferencing the Access Type and calling the right function:
-- LED blink if CLINT.Machine_Time > led_timer then if GPIO.Set (Red_LED) or GPIO.Set (Green_LED) or GPIO.Set (Blue_LED) then All_LEDs_Off; led_timer := CLINT.Machine_Time + LED_OFF_TIME; else All_LEDs_Off; Turn_On (LED.all); led_timer := CLINT.Machine_Time + LED_ON_TIME; end if; end if;