Encryption Service Userspace Application
This submodule guides you through creating and using an encryption service. Providing an encryption service is a common feature of a HWRoT.
An encryption service can cryptographically encrypt and/or decrypt data on behalf of an application. Crucially, the service encapsulates the encryption key, preventing the application from ever having access to the key. This also prevents an attacker from being able to retrieve the key, enhancing the security of the encrypted data.
Once we setup the encryption service, we will use it throughout this course to demonstrate Tock's strengths as a hardware root of trust OS.
Background
Secure Elements as Roots of Trust
Recall from the overview that a secure element is a purpose-built chip used for key storage and encryption purposes, often in support of a main processor which needs to perform some kind of cryptography.
In a real-world setting, a secure element like the Infineon SLE78 (the chip used in the YubiKey 5 series) might communicate over a standard device-internal bus like SPI or I2C, or might even communicate directly with a host over USB.
Often, smaller secure elements like the SLE78 will receive commands and deliver responses encoded as application protocol data units (APDUs), a holdover from the smart card industry.
While we could replicate this behavior for our encryption service by passing APDUs back and forth over USB, we elide this complexity for the sake of simplicity and just prompt the user for plaintext to encrypt via Tock's console.
Hardware-backed Keys
In an actual hardware root of trust, the AES key in the encryption oracle would be hardware-backed, i.e. it would be generated and kept in a hardware key store apart from where the processor could directly access it.
While the nRF5x AES peripheral doesn't have support for hardware-backed keys, it does allow us to store our AES128 key in the encryption oracle driver and load it into the AES128 peripheral as needed; this is almost as secure, and in any case the difference is invisible to the userspace application which can't access the key either way.
Applications in Tock
For readers who have previously written embedded software, it's important to note that Tock applications are written in a manner much more similar to traditional, non-embedded software. They are compiled separately from the kernel and loaded separately onto the hardware. They can be started or stopped individually and can be removed from the hardware individually. Moreover, the kernel decides which applications to run and what permissions they should be given.
Applications make requests to the OS kernel through system calls. Applications
instruct the kernel using Command system calls, and the kernel notifies
applications with upcalls the application must subscribe
to. Importantly,
upcalls never interrupt a running application. The application must yield
to
receive upcalls (i.e. callbacks).
The userspace library (libtock
) wraps system calls in easier to use functions.
The libtock library is completely asynchronous. Synchronous APIs to the system
calls are in libtock-sync
. These functions include the call to yield
and
expose a synchronous driver interface. Application code can use either.
Tock Allows and Upcalls
When interacting with drivers in Tock, it's important to note that by design, any driver can only access the data you explicitly allow it access to. In Tock, an allow is a buffer shared from a userspace application to a specified driver. These can be read-only, where the driver can only read what the app supplies in the buffer, or read-write, where the driver can also modify the buffer to e.g. write results.
In order to easily allow asynchronous driver interfaces, the Tock driver allows registering upcalls, callbacks which kernel drivers can invoke e.g. to signal to an app that a requested operation has completed.
Inter-process Communication (IPC) in Tock
Tock has an IPC driver in the kernel which allows userspace apps to advertise
IPC services with names such as org.tockos.tutorial.led_service
.
Applications that want to make requests over IPC can use the ipc_discover()
function with an IPC service name to fetch the application ID of the app hosting
the service. After this is done, the requesting app can register callbacks,
allow access to shared buffers, and finally notify
the IPC service to perform
some operation.
Submodule Overview
We will be developing two userspace applications. The first provides a user interface with the screen and buttons. The second is the encryption service application. An overview of the structure is here:
┌──────────────────┐ ┌────────────────────┐
│ │ │ │
│ Screen App │ IPC │ Encryption Service │
│ │◄─────►│ App │
│ UI + Logging │ │ │
│ │ │ │
└───┬───────┬──────┘ └──┬───────────┬─────┘
Userspace │ │ │ │
─────────────────┼───────┼─────────────────┼───────────┼──────
Tock ┌──▼───┐┌──▼────┐ ┌─────▼────────┐┌─▼─────┐
Kernel │Screen││Buttons│ │AES Encryption││Console│
└──────┘└───────┘ │Oracle │└───────┘
└──────────────┘
The two applications will communicate using IPC. We will focus on creating the encryption service app, and use the screen app to help interact with the user.
Each application uses capsules provided by the kernel. The capsules have already been created and are already included with the kernel we installed. Consistent with our goal for an HWRoT encryption service, the AES Encryption Oracle has a built-in AES key that we can use to encrypt messages without our userspace application ever making contact with the key itself.
Milestones
We have three small milestones in this section, all of which build upon supplied starter code.
- Milestone one adds support for interfacing with a dispatch/logging service, to illustrate how various root of trust services might be dispatched in practice while maintaining separation.
- Milestone two adds support for sending/receiving data and serializing plaintext, introducing how libtock-c APIs work.
- Milestone three adds actual encryption support using the encryption oracle driver, demonstrating how the Tock syscall interface.
Setup
Before starting, check the following:
-
Make sure you have compiled and installed the Tock kernel with the screen and encryption oracle drivers on to your board.
cd tock/boards/tutorials/nrf52840dk-root-of-trust-tutorial make install
-
Make sure you have no testing apps installed. To remove all apps:
tockloader erase-apps
Starter Code
We'll start with the starter code, which includes a logging application for displaying encryption service logs to the OLED screen, as well as a scaffold for developing the remainder of the encryption service userspace app.
-
Inside your copy of
libtock-c
, navigate tolibtock-c/examples/tutorials/root_of_trust/
.This contains the starter code which you'll work from in the following steps. For now, all this application does is present a list of services that the root of trust can provide, and allows you to select one to interact with.
-
Compile the screen application and load it onto your board. In the
screen/
subdirectory, runmake install
-
Next, navigate to the
encrypt_service/
subdirectory in the same parent folder and load it as well by again runningmake install
-
After both applications are loaded, you should see a screen which should allow you to select a service to dispatch. You can navigate up and down in the menu by using
BUTTON 1
andBUTTON 3
on the nRF52840dk board, and you can select an option by pressingBUTTON 2
and then clickingStart
.Note that right now, the encryption service doesn't have any code to react to requests for dispatch, so if you select it nothing will happen.
The source code for the screen application is in screen/main.c
. If you dig
through it, you'll find logic for
- displaying the menu to select an application,
- requesting a root of trust service to be dispatched on select
- listening for logging requests to display
The macros for generating a menu using the u8g2
user interface library are a
bit obtuse at first, so they (along with the rest of the file) have been
commented thoroughly.
Milestone One: Connecting to the Main Screen App
To begin, we first want our encryption service to be able to (a) respond when the main screen app signals it to take over control of the UART console, and (b) connect to the logging service the main screen app provides for displaying logs to the screen.
From a functionality standpoint, we certainly could have service dispatch, all of the desired cryptographic services, and logging functionality in one event loop in an application; however, when using Tock, separating these functionalities into different apps is helpful from a security perspective. We'll discuss this more in the next part of the tutorial.
First, let's modify our scaffold in encryption_service_starter/main.c
to
respond to the main screen app's dispatch signal using Tock's inter-process
communication (IPC) driver. Rename the directory in your local copy from
encryption_service_starter/
to encryption_service/
. Completed code is
available in encryption_service_milestone_one/
if you run into issues.
-
Take a look in
screen/main.c
at theselect_rot_service()
function. This function, called bymain()
, takes in the name of an IPC service hosted e.g. by our encryption service app and- calls
ipc_discover()
to go from the IPC service name to the ID of the process hosting that IPC service - calls
ipc_register_service_callback()
to register a logging IPC service underorg.tockos.tutorials.root_of_trust.screen
, so that the selected root of trust service can log to screen - calls
ipc_notify_service()
to trigger the IPC service of the process whose IDipc_process()
found
All of these IPC API functions are provided by the
libtock/kernel/ipc.h
header included at the top of the file. - calls
-
To start, open
encryption_service/main.c
and create await_for_start()
function which registers an IPC callback under the service nameorg.tockos.tutorials.root_of_trust.encryption_service
and then yields until that callback is triggered by the main screen app.- You'll want to use the IPC function
ipc_register_service_callback()
to register your callback function. See the documentation there for how the signature of your callback function should look. - The callback function you write should set a global
bool
from false to true.wait_for_start()
can then use theyield_for
function to wait for this change in state.
- You'll want to use the IPC function
-
Now, call
wait_for_start()
inmain()
, and follow it with a call toprintf()
to send a message to the UART console; this should indicate when your app has been selected. -
To test this out, build and install your application as previous, then run
tockloader listen
in a separate terminal. When you select the encryption service and hitStart
in the menu, you should see your message in the console (not on the screen).TIP: You can leave the console running, even when compiling and uploading new applications. It's worth opening a second terminal and leaving
tockloader listen
always running.
Next, we need to connect our application back to the screen logging IPC service. To do this,
-
Again in
encryption_service/main.c
, create a newsetup_logging()
function which takes in a message string and sends it via IPC to the logging service to display.-
In the
log_to_screen()
function, you'll want to useipc_discover()
to discover the process ID for the logging service,ipc_register_client_callback()
to provide a callback that sets a global flag to indicate a completed log, andipc_share()
to share a buffer to the logging service. -
When creating the log buffer to share over IPC, which can store as long of a message as will fit on the OLED screen. 32 bytes should be sufficient. Make sure that the buffer is marked with the
aligned
attribute. i.e.char log_buffer[LOG_WIDTH] __attribute__((aligned(LOG_WIDTH)));
-
-
Next, create a new
log_to_screen()
function which takes in a null-terminated message string and sends it via IPC to the logging service to display.- To trigger the logging service to fetch your message string from the shared
buffer, you'll want to use
ipc_notify()
.
- To trigger the logging service to fetch your message string from the shared
buffer, you'll want to use
-
To test your implementation, add calls to
wait_for_start()
andsetup_logging()
tomain()
, and follow them with some calls tolog_to_screen()
.- To test your implementation, recompile and re-install your encryption app and then use the on-device menu to start the encryption service.
Checkpoint: Your application should now be able to receive encryption requests over the UART console, and log these requests over IPC to the screen.
Milestone Two: Sending/Receiving Data and Serializing Plaintext
Now that we can interact with the main screen app over IPC, we should set up the UART console to allow inputting secrets to encrypt, and make sure that we can encode the resulting ciphertext as hex to present back to the user.
In a practical HWRoT setting, it may be inadvisable to send secret values to a
device in the clear
where they could be intercepted. For instance, smart cards
and smaller secure elements often make use of GlobalPlatform's Secure Channel
Protocols such as
Secure Channel Protocol 03
to establish an encrypted, authenticated channel before exchanging any secret
information.
For brevity, we won't implement a full secure channel in this protocol, but at the end of this section we include a challenge in this vein for after the tutorial is complete.
To start, let's retrieve the secret from the user over UART to parse. Completed
code is available in encryption_service_milestone_two/
if you run into issues.
-
In
encryption_service/main.c
, create arequest_plaintext()
function which prompts a user over UART for plaintext into a provided buffer with a provided size.-
To prompt the user, you'll want to use
libtocksync_console_write()
with a message like"Enter a secret to encrypt:"
. -
For fetching a response, you'll want to use
libtocksync_console_read()
to read bytes one-by-one, breaking when you hit a newline (\n
or\r
). You'll also want to use this function to strip leading whitespace from the user's input. -
Make sure to echo each character as it's received by writing it back, or else the user won't be able to see their input.
-
For convenience later, return the size of the input.
-
-
Next, we'll add a function
bytes_to_hex()
which inputs a byte buffer and length, and outputs a null-terminated hex string.- When writing this function, the most direct way to convert a byte to hex is
with
sprintf
: you can use"%02X"
as a format string.
- When writing this function, the most direct way to convert a byte to hex is
with
-
To test both of these functions in concert, modify
main
to, in a loop, input plaintext over the console, convert it to hex, and then report it to the screen.
Checkpoint: Your application should now be able to input messages via the UART interface and report byte values as hex.
Milestone Three: Adding Encryption Support
Finally, we want to actually encrypt our messages before we report them.
NOTE: If you've completed the HOTP tutorial prior, the same implementation of
oracle.c
there will work--feel free to simply copy it over fromencryption_service_milestone_three/
if you've already implemented it before.
We first create a new file to house our interface to the encryption oracle
driver, then integrate it into main
:
-
Create a header file
oracle.h
inencryption_service/
with the following prototype (don't forget to#include <stdint.h>
!):int oracle_encrypt(const uint8_t* plaintext, int plaintext_len, uint8_t* output, int output_len, uint8_t iv[16]);
-
Create a source file
oracle.c
next tooracle.h
with an implementation of this function, using the encryption oracle to encryptplaintext
and placing the result inoutput_len
. Theiv
buffer should be used to return the randomized initialization vector generated for encryption.-
To randomize the IV, you'll want to use
libtocksync_rng_get_random_bytes()
. -
The current kernel configuration has the ID for the encryption capsule as
0x99999
, which you'll pass to each command that targets it. -
From there, the driver requires three allows to operate; you'll want to use
allow_readonly()
andallow_readwrite()
to set them up.- A read-only allow with ID 0 for sending the input plaintext
- A read-only allow with ID 1 for sending the input IV
- A read-write allow with ID 0 for receiving the output ciphertext
-
Next, you'll need to set up an upcall to confirm when the encryption is done. You'll want the signature of your upcall to look like
static void crypt_upcall(__attribute__((unused)) int num, int len, __attribute__ ((unused)) int arg2, __attribute__ ((unused)) void* ud);
and it should both set a global flag indicating that the upcall is done, as well as store the ciphertext length passed to it in a global variable.
-
Finally, you'll need to send a command to the driver with command ID 1 to trigger the start, after which you should
yield_for()
until the upcall completes and reset thedone
flag for the next call. The return value of the function should be the length returned from the upcall.
-
-
Finally, let's wire it all together. Go back to
main()
inmain.c
, and make it do the following:- Wait for the start signal for the encryption service (
wait_for_start()
) - Set up the IPC logging interface (
setup_logging()
) - Looping forever (using
log_to_screen
to indicate each step is happening):- Request a plaintext from the user (
request_plaintext()
), from a plaintext buffer and to ciphertext buffer both of size512
(four AES-128 blocks) - Encrypt the plaintext (
oracle_encrypt()
, fromoracle.c
) - Convert the ciphertext to a hex string (
bytes_to_hex()
) - Dump the ciphertext to the console (e.g. with
printf("Ciphertext: %s\n", ...)
)
- Request a plaintext from the user (
- Wait for the start signal for the encryption service (
When you run the test now, you should be able to use tockloader listen
and
type messages into the UART console when prompted to encrypt them.
Checkpoint: Your application should now be able to encrypt arbitrary messages sent over the UART console, logging the status of the encryption capsule to the screen as it runs.
Submodule Complete
Congratulations! Feel free to move on to the next section, where we'll begin to attack our implementation and show how Tock allows for defense-in-depth measures appropriate for a root of trust operating system.
If you have additional time or are looking to deepen your knowledge of Tock, continue to challenge section below.
Challenge: Authenticating the Results
NOTE: This challenge is open-ended, may take a while, and requires experience working on Tock drivers--it's best approached after completing the remainder of the tutorial. We'll touch on Tock drivers later in this tutorial, but you can also follow the HOTP tutorial for additional practice if you'd like.
As mentioned earlier, communication channels with a HWRoT are often encrypted and authenticated. The former provides confidentiality so that secrets can't be extracted by eavesdroppers; meanwhile, the latter provides authenticity of results so attackers can't impersonate either party.
While designing a secure channel is a surprisingly tricky task, many existing frameworks exist, e.g. the popular Noise Protocol Framework used by many projects including the well-known WireGuard VPN. As a step in this direction, the challenge described here is to just provide authentication using ECDSA signatures for the ciphertexts that the root of trust produces, so that a client of the encryption service can be sure that the results they receive came from our root of trust.
Here is an outline for how one might go about doing so--these steps are intentionally a bit vague, as this is intended more to serve as a longer-term practice than something that can be done in the timeframe of an in-person tutorial:
-
You'll want to first add a signing oracle driver. While the nRF52840dk board used for this tutorial lacks ECDSA hardware support[^1], Tock provides an
ecdsa-sw
driver which wraps RustCrypto's signing and verifying implementations to provide software support.-
The actual structure you will want to use is the
EcdsaP256SignatureSigner
incapsules/ecdsa_sw/src/p256_signer.rs
. This struct implements thepublic_key_crypto::SignatureSign
hardware interface layer (HIL) trait, so you can use itssign()
method to sign messages and itsset_sign_client()
to designate a callback for when a signing operation is completed. Thepublic_key_crypto::SetKey
HIL will similarly allow you to change the key the signer uses. -
You can base your work off the encryption oracle implementation in
capusles/extra/tutorials/encryption_oracle_chkpt5.rs
. Most of the logic for tracking driver state should remain the same, but instead of the driver struct containing an instance of an AES struct used encrypt, your driver struct will contain anEcdsaP256SignatureSigner
used to sign.
-
-
Next, you'll want to create a board definition based off the one in
boards/nordic/nrf52840dk/src/main.rs
which instantiates aEcdsaP256SignatureSigner
and your signing oracle driver, passing the former to the latter on creation.- For an example this struct in use, see the ECDSA test capsule in
capsules/ecdsa_sw/src/test/p256.rs
as well as the test board configuration inboards/configurations/nrf52840dk/nrf52840dk-test-kernel/src/test/ecdsa_p256_test.rs
which depends on it.
- For an example this struct in use, see the ECDSA test capsule in
-
Finally, you'll want to create a new userspace interface to this driver akin to that in
encryption_service/oracle.c
. The resulting file should be almost identical, but of course with functions accepting messages to sign instead of secrets to encrypt, etc.
Even if you don't complete all these steps, hopefully reviewing the above outline should give a good picture of how you can go from an idea of a driver you need for an application to a full implementation and integration into a userspace app.
[^1] This is almost true: the nRF52840 chip contains the closed-source ARM TrustZone CryptoCell 310, which has support for ECDSA signatures, but sadly there's not driver support for it yet (due to its closed-source nature).