Encryption Oracle Capsule

Our HOTP security key works by storing a number of secrets on the device, and using these secrets together with some moving factor (e.g., a counter value or the current time) in an HMAC operation. This implies that our device needs some way to store these secrets, for instance in its internal flash.

However, storing such secrets in plaintext in ordinary flash is not particularly secure. For instance, many microcontrollers offer debug ports which can be used to gain read and write access to flash. Even if these ports can be locked down, such protection mechanisms have been broken in the past. Apart from that, disallowing external flash access makes debugging and updating our device much more difficult.

To circumvent these issues, we will build an encryption oracle capsule: this Tock kernel module will allow applications to request decryption of some ciphertext, using a kernel-internal key not exposed to applications themselves. By only storing an encrypted version of their secrets, applications are free to use unprotected flash storage, or store them even external to the device itself. This is a commonly used paradigm in root of trust systems such as TPMs or OpenTitan, which feature hardware-embedded keys that are unique to a chip and hardened against key-readout attacks.

Our kernel module will use a hard-coded symmetric encryption key (AES-128 CTR-mode), embedded in the kernel binary. While this does not actually meaningfully increase the security of our example application, it demonstrates some important concepts in Tock:

  • How custom userspace drivers are implemented, and the different types of system calls supported.
  • How Tock implements asynchronous APIs in the kernel.
  • Tock's hardware-interface layers (HILs), which provide abstract interfaces for hardware or software implementations of algorithms, devices and protocols.

Capsules – Tock's Kernel Modules

Most of Tock's functionality is implemented in the form of capsules – Tock's equivalent to kernel modules. Capsules are Rust modules contained in Rust crates under the capsules/ directory within the Tock kernel repository. They can be used to implement userspace drivers, hardware drivers (for example, a driver for an I²C-connected sensor), or generic reusable code snippets.

What makes capsules special is that they are semi-trusted: they are not allowed to contain any unsafe Rust code, and thus can never violate Tock's memory safety guarantees. They are only trusted with respect to liveness and correctness – meaning that they must not block the kernel execution for long periods of time, and should behave correctly according to their specifications and API contracts.

We start our encryption oracle driver by creating a new capsule called encryption_oracle. Create a file under capsules/extra/src/tutorials/encryption_oracle.rs in the Tock kernel repository with the following contents:

#![allow(unused)]
fn main() {
// Licensed under the Apache License, Version 2.0 or the MIT License.
// SPDX-License-Identifier: Apache-2.0 OR MIT
// Copyright Tock Contributors 2022.

pub static KEY: &'static [u8; kernel::hil::symmetric_encryption::AES128_KEY_SIZE] =
    b"InsecureAESKey12";

pub struct EncryptionOracleDriver {}

impl EncryptionOracleDriver {
    /// Create a new instance of our encryption oracle userspace driver:
    pub fn new() -> Self {
        EncryptionOracleDriver {}
    }
}

}

We will be filling this module with more interesting contents soon. To make this capsule accessible to other Rust modules and crates, add it to capsules/extra/src/tutorials/mod.rs:

  #[allow(dead_code)]
  pub mod encryption_oracle_chkpt5;

+ pub mod encryption_oracle;

EXERCISE: Make sure your new capsule compiles by running cargo check in the capsules/extra/ folder.

The capsules/tutorial crate already contains checkpoints of the encryption oracle capsule we'll be writing here. Feel free to use them if you're stuck. We indicate that your capsule should have reached an equivalent state to one of our checkpoints through blocks such as the following:

CHECKPOINT: encryption_oracle_chkpt0.rs

BACKGROUND: While a single "capsule" is generally self-contained in a Rust module (.rs file), these modules are again grouped into Rust crates such as capsules/core and capsules/extra, depending on certain policies. For instance, capsules in core have stricter requirements regarding their code quality and API stability. Neither core nor the extra extra capsules crates allow for external dependencies (outside of the Tock repository). The document on external dependencies further explains these policies.

Userspace Drivers

Now that we have a basic capsule skeleton, we can think about how this code is going to interact with userspace applications. Not every capsule needs to offer a userspace API, but those that do must implement the SyscallDriver trait.

Tock supports different types of application-issued systems calls, four of which are relevant to userspace drivers:

  • subscribe: An application can issue a subscribe system call to register upcalls, which are functions being invoked in response to certain events. These upcalls are similar in concept to UNIX signal handlers. A driver can request an application-provided upcall to be invoked. Every system call driver can provide multiple "subscribe slots", each of which the application can register a upcall to.

  • read-only allow: An application may expose some data for drivers to read. Tock provides the read-only allow system call for this purpose: an application invokes this system call passing a buffer, the contents of which are then made accessible to the requested driver. Every driver can have multiple "allow slots", each of which the application can place a buffer in.

  • read-write allow: Works similarly to read-only allow, but enables drivers to also mutate the application-provided buffer.

  • command: Applications can use command-type system calls to signal arbitrary events or send requests to the userspace driver. A common use-case for command-style systems calls is, for instance, to request that a driver start some long-running operation.

All Tock system calls are synchronous, which means that they should immediately return to the application. In fact, subscribe and allow-type system calls are transparently handled by the kernel, as we will see below. Capsules must not implement long-running operations by blocking on a command system call, as this prevents other applications or kernel routines from running – kernel code is never preempted.

Application Grants

Now there's just one key part missing to understanding Tock's system calls: how drivers store application-specific data. Tock differs significantly from other operating systems in this regard, which typically simply allocate some memory on demand through a heap allocator.

However, on resource constraint platforms such as microcontrollers, allocating from a pool of (limited) memory can inevitably become a prominent source of resource exhaustion errors: once there's no more memory available, Tock wouldn't be able to service new allocation requests, without revoking some prior allocations. This is especially bad when this memory pool is shared between kernel resources belonging to multiple processes, as then one process could potentially starve another.

To avoid these issues, Tock uses grants. A grant is a memory allocation belonging to a process, and is located within a process-assigned memory allocation, but reserved for use by the kernel. Whenever a kernel component must keep track of some process-related information, it can use a grant to hold this information. By allocating memory from a process-specific memory region it is impossible for one process to starve another's memory allocations, independent of whether those allocations are in the process itself or in the kernel. As a consequence, Tock can avoid implementing a kernel heap allocator entirely.

Ultimately, our encryption oracle driver will need to keep track of some per-process state. Thus we extend the above driver with a Rust struct to be stored within a grant, called App. For now, we just keep track of whether a process has requested a decryption operation. Add the following code snippet to your capsule:

#![allow(unused)]
fn main() {
#[derive(Default)]
pub struct ProcessState {
    request_pending: bool,
}
}

By implementing Default, grant types can be allocated and initialized on demand. We integrate this type into our EncryptionOracleDriver by adding a special process_grants variable of type Grant. This Grant struct takes a generic type parameter T (which we set to our ProcessState struct above) next to some constants: as a driver's subscribe upcall and allow buffer slots also consume some memory, we store them in the process-specific grant as well. Thus, UpcallCount, AllowRoCont, and AllowRwCount indicate how many of these slots should be allocated respectively. For now we don't use any of these slots, so we set their counts to zero. Add the process_grants variable to your EncryptionOracleDriver:

#![allow(unused)]
fn main() {
use kernel::grant::{Grant, UpcallCount, AllowRoCount, AllowRwCount};

pub struct EncryptionOracleDriver {
    process_grants: Grant<
        ProcessState,
        UpcallCount<0>,
        AllowRoCount<0>,
        AllowRwCount<0>,
    >,
}
}

EXERCISE: The Grant struct will be provided as an argument to constructor of the EncryptionOracleDriver. Extend new to accept it as an argument. Afterwards, make sure your code compiles by running cargo check in the capsules/extra/ directory.

Implementing a System Call

Now that we know about grants we can start to implement a proper system call. We start with the basics and implement a simple command-type system call: upon request by the application, the Tock kernel will call a method in our capsule.

For this, we implement the following SyscallDriver trait for our EncryptionOracleDriver struct. This trait contains two important methods:

  • command: this method is called whenever an application issues a command-type system call towards this driver, and
  • allocate_grant: this is a method required by Tock to allocate some space in the process' memory region. The implementation of this method always looks the same, and while it must be implemented by every userspace driver, it's exact purpose is not important right now.
#![allow(unused)]
fn main() {
use kernel::{ErrorCode, ProcessId};
use kernel::syscall::{SyscallDriver, CommandReturn};

impl SyscallDriver for EncryptionOracleDriver {
    fn command(
        &self,
        command_num: usize,
        _data1: usize,
        _data2: usize,
        processid: ProcessId,
    ) -> CommandReturn {
        // Syscall handling code here!
        unimplemented!()
    }

    // Required by Tock for grant memory allocation.
    fn allocate_grant(&self, processid: ProcessId) -> Result<(), kernel::process::Error> {
        self.process_grants.enter(processid, |_, _| {})
    }
}
}

The function signature of command tells us a lot about what we can do with this type of system call:

  • Applications can provide a command_num, which indicates what type of command they are requesting to be handled by a driver, and
  • they can optionally pass up to two usize data arguments.
  • The kernel further provides us with a unique identifier of the calling process, through a type called ProcessId.

Our driver can respond to this system call using a CommandReturn struct. This struct allows for returning either a success or a failure indication, along with some data (at most four usize return values). For more details, you can look at its definition and API here.

In our encryption oracle driver we only need to handle a single application request: to decrypt some ciphertext into its corresponding plaintext. As we are missing the actual cryptographic operations still, let's simply store that a process has made such a request. Because this is per-process state, we store it in the request_pending field of the process' grant region. To obtain a reference to this memory, we can conveniently use the ProcessId type provided to us by the kernel. The following code snippet shows how an implementation of the command could look like. Replace your command method body with this snippet:

#![allow(unused)]
fn main() {
match command_num {
    // Check whether the driver is present:
    0 => CommandReturn::success(),

    // Request the decryption operation:
    1 => {
        self
            .process_grants
            .enter(processid, |app, _kernel_data| {
			    kernel::debug!("Received request from process {:?}", processid);
                app.request_pending = true;
                CommandReturn::success()
            })
            .unwrap_or_else(|err| err.into())
    },

    // Unknown command number, return a NOSUPPORT error
    _ => CommandReturn::failure(ErrorCode::NOSUPPORT),
}
}

There's a lot to unpack here: first, we match on the passed command_num. By convention, command number 0 is reserved to check whether a driver is loaded on a kernel. If our code is executing, then this must be the case, and thus we simply return success. For all other unknown command numbers, we must instead return a NOSUPPORT error.

Command number 1 is assigned to start the decryption operation. To get a reference to our process-local state stored in its grant region, we can use the enter method: it takes a ProcessId, and in return will call a provided Rust closure that provides us access to the process' own ProcessState instance. Because entering a grant can fail (for instance when the process does not have sufficient memory available), we handle any errors by converting them into a CommandReturn.

EXERCISE: Make sure that your EncryptionOracleDriver implements the SyscallDriver trait as shown above. Then, verify that your code compiles by running cargo check in the capsules/extra/ folder.

CHECKPOINT: encryption_oracle_chkpt1.rs

Congratulations, you have implemented your first Tock system call! Next, we will look into how to to integrate this driver into a kernel build.

Adding a Capsule to a Tock Kernel

To actually make our driver available in a given build of the kernel, we need to add it to a board crate. Board crates tie the kernel, a given chip, and a set of drivers together to create a binary build of the Tock operating system, which can then be loaded into a physical board. For the purposes of this section, we assume to be targeting the Nordic Semiconductor nRF52840DK board, and thus will be working in the boards/nordic/nrf52840dk/ directory.

EXERCISE: Enter the boards/nordic/nrf52840dk/ directory and compile a kernel by typing make. A successful build should end with a message that looks like the following:

    Finished release [optimized + debuginfo] target(s) in 20.34s
   text    data     bss     dec     hex filename
 176132       4   33284  209420   3320c /home/tock/tock/target/thumbv7em-none-eabi/release/nrf52840dk
[Hash ommitted]  /home/tock/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin

Applications interact with our driver by passing a "driver number" alongside their system calls. The capsules/core/src/driver.rs module acts as a registry for driver numbers. For the purposes of this tutorial we'll use an unassigned driver number in the misc range, 0x99999, and add a constant to capsule accordingly:

#![allow(unused)]
fn main() {
pub const DRIVER_NUM: usize = 0x99999;
}

Accepting an AES Engine in the Driver

Before we start adding our driver to the board crate, we'll modify it slightly to acceppt an instance of an AES128 cryptography engine. This is to avoid modifying our driver's instantiation later on. We provide the encryption_oracle_chkpt2.rs checkpoint which has these changes integrated, feel free to use this code. We make the following mechanical changes to our types and constructor – don't worry about them too much right now.

First, we change our EncryptionOracleDriver struct to hold a reference to some generic type A, which must implement the AES128 and the AESCtr traits:

+ use kernel::hil::symmetric_encryption::{AES128Ctr, AES128};

- pub struct EncryptionOracleDriver {
+ pub struct EncryptionOracleDriver<'a, A: AES128<'a> + AES128Ctr> {
+     aes: &'a A,
      process_grants: Grant<
          ProcessState,
          UpcallCount<0>,

Then, we change our constructor to accept this aes member as a new argument:

- impl EncryptionOracleDriver {
+ impl<'a, A: AES128<'a> + AES128Ctr> EncryptionOracleDriver<'a, A> {
      /// Create a new instance of our encryption oracle userspace driver:
      pub fn new(
+         aes: &'a A,
+         _source_buffer: &'static mut [u8],
+         _dest_buffer: &'static mut [u8],
          process_grants: Grant<ProcessState, UpcallCount<0>, AllowRoCount<0>, AllowRwCount<0>>,
      ) -> Self {
          EncryptionOracleDriver {
              process_grants: process_grants,
+             aes: aes,
          }
      }
  }

And finally we update our implementation of SyscallDriver to match these new types:

- impl SyscallDriver for EncryptionOracleDriver {
+ impl<'a, A: AES128<'a> + AES128Ctr> SyscallDriver for EncryptionOracleDriver<'a, A> {
      fn command(
          &self,

Finally, make sure that your modified capsule still compiles.

CHECKPOINT: encryption_oracle_chkpt2.rs

Instantiating the System Call Driver

Now, open the board's main file (boards/nordic/nrf52840dk/src/main.rs) and scroll down to the line that reads "PLATFORM SETUP, SCHEDULER, AND START KERNEL LOOP". We'll instantiate our encryption oracle driver right above that, with the following snippet:

#![allow(unused)]
fn main() {
const CRYPT_SIZE: usize = 7 * kernel::hil::symmetric_encryption::AES128_BLOCK_SIZE;
let aes_src_buffer = kernel::static_init!([u8; 16], [0; 16]);
let aes_dst_buffer = kernel::static_init!([u8; CRYPT_SIZE], [0; CRYPT_SIZE]);

let oracle = static_init!(
    capsules_extra::tutorials::encryption_oracle::EncryptionOracleDriver<
        'static,
        nrf52840::aes::AesECB<'static>,
    >,
    // Call our constructor:
    capsules_extra::tutorials::encryption_oracle::EncryptionOracleDriver::new(
        &base_peripherals.ecb,
        aes_src_buffer,
        aes_dst_buffer,
		// Magic incantation to create our `Grant` struct:
        board_kernel.create_grant(
            capsules_extra::tutorials::encryption_oracle::DRIVER_NUM, // our driver number
            &create_capability!(capabilities::MemoryAllocationCapability)
        ),
    ),
);

// Leave commented out for now:
// kernel::hil::symmetric_encryption::AES128::set_client(&base_peripherals.ecb, oracle);
}

Now that we instantiated our capsule, we need to wire it up to Tock's system call handling facilities. This involves two steps: first, we need to store our instance in our Platform struct. That way, we can refer to our instance while the kernel is running. Then, we need to route system calls to our driver number (0x99999) to be handled by this driver.

Add the following line to the very bottom of the pub struct Platform { declaration:

  pub struct Platform {
      [...],
      systick: cortexm4::systick::SysTick,
+     oracle: &'static capsules_extra::tutorials::encryption_oracle::EncryptionOracleDriver<
+         'static,
+         nrf52840::aes::AesECB<'static>,
+     >,
  }

Furthermore, add our instantiated oracle to the let platform = Platform { instantiation:

  let platform = Platform {
      [...],
      systick: cortexm4::systick::SysTick::new_with_calibration(64000000),
+     oracle,
  };

Finally, to handle received system calls in our driver, add the following line to the match block in the with_driver method of the SyscallDriverLookup trait implementation:

  impl SyscallDriverLookup for Platform {
      fn with_driver<F, R>(&self, driver_num: usize, f: F) -> R
      where
          F: FnOnce(Option<&dyn kernel::syscall::SyscallDriver>) -> R,
      {
          match driver_num {
              capsules_core::console::DRIVER_NUM => f(Some(self.console)),
              [...],
              capsules_extra::app_flash_driver::DRIVER_NUM => f(Some(self.app_flash)),
+             capsules_extra::tutorials::encryption_oracle::DRIVER_NUM => f(Some(self.oracle)),
              _ => f(None),
          }
      }
  }

That's it! We have just added a new driver to the nRF52840DK's Tock kernel build.

EXERCISE: Make sure your board compiles by running make. If you want, you can test your driver with a libtock-c application which executes the following:

command(
    0x99999, // driver number
    1,       // command number
    0, 0     // optional data arguments
);

Upon receiving this system call, the capsule should print the "Received request from process" message.

Interacting with HILs

The Tock operating system supports different hardware platforms, each featuring an individual set of integrated peripherals. At the same time, a driver such as our encryption oracle should be portable between different systems running Tock. To achieve this, Tock uses the concept of Hardware-Interface Layers (HILs), the design paradigms of which are described in this document. HILs are organized as Rust modules, and can be found under the kernel/src/hil/ directory. We will be working with the symmetric_encryption.rs HIL.

HILs capture another important concept of the Tock kernel: asynchronous operations. As mentioned above, Tock system calls must never block for extended periods of time, as kernel code is not preempted. Blocking in the kernel prevents other useful being done. Instead, long-running operations in the Tock kernel are implemented as asynchronous two-phase operations: one function call on the underlying implementation (e.g., of our AES engine) starts an operation, and another function call (issued by the underlying implementation, hence named callback) informs the driver that the operation has completed. You can see this paradigm embedded in all of Tock's HILs, including the symmetric_encryption HIL: the crypt() method is specified to return immediately (and return a Some(_) in case of an error). When the requested operation is finished, the implementor of AES128 will call the crypt_done() callback, on the client registered with set_client().

The below figure illustates the way asynchronous operations are handled in Tock, using our encryption oracle capsule as an example. One further detail illustrated in this figure is the fact that providers of a given interface (e.g., AES128) may not always be able to perform a large user-space operation in a single call; this may be because of hardware-limitations, limited buffer allocations, or to avoid blocking the kernel for too long in software-implentations. In this case, a userspace-operation is broken up into multiple smaller operations on the underlying provider, and the next sub-operation is scheduled once a callback has been received:

An Illustration of Tock's Asynchronous Driver Model

To allow our capsule to receive crypt_done callbacks, add the following trait implementation:

#![allow(unused)]
fn main() {
use kernel::hil::symmetric_encryption::Client;

impl<'a, A: AES128<'a> + AES128Ctr> Client<'a> for EncryptionOracleDriver<'a, A> {
    fn crypt_done(&'a self, mut source: Option<&'static mut [u8]>, destination: &'static mut [u8]) {
	    unimplemented!()
    }
}
}

With this trait implemented, we can wire up the oracle driver instance to receive callbacks from the AES engine (base_peripherals.ecb) by uncommenting the following line in boards/nordic/nrf52840dk/src/main.rs:

- // Leave commented out for now:
- // kernel::hil::symmetric_encryption::AES128::set_client(&base_peripherals.ecb, oracle);
+ kernel::hil::symmetric_encryption::AES128::set_client(&base_peripherals.ecb, oracle);

If this is missing, our capsule will not be able to receive feedback from the AES hardware that an operation has finished, and it will thus refuse to start any new operation. This is an easy mistake to make – you should check whether all callbacks are set up correctly when the kernel is in such a stuck state.

Multiplexing Between Processes

While our underlying AES128 implementation can only handle one request at a time, multiple processes may wish to use this driver. Thus our capsule implements a queueing system: even when another process is already using our capsule to decrypt some ciphertext, another process can still initate such a request. We remember these requests through the request_pending flag in our ProcessState grant, and we've already implemented the logic to set this flag!

Now, to actually implement our asynchronous decryption operation, it is further important to keep track of which process' request we are currently working on. We add an additional state field to our EncryptionOracleDriver holding an OptionalCell: this is a container whose stored value can be modified even if we only hold an immutable Rust reference to it. The optional indicates that it behaves similar to an Option – it can either hold a value, or be empty.

  use kernel::utilities::cells::OptionalCell;

  pub struct EncryptionOracleDriver<'a, A: AES128<'a> + AES128Ctr> {
      aes: &'a A,
      process_grants: Grant<ProcessState, UpcallCount<0>, AllowRoCount<0>, AllowRwCount<0>>,
+     current_process: OptionalCell<ProcessId>,
  }

We need to add it to the constructor as well:

  pub fn new(
      aes: &'a A,
      _source_buffer: &'static mut [u8],
      _dest_buffer: &'static mut [u8],
      process_grants: Grant<ProcessState, UpcallCount<0>, AllowRoCount<0>, AllowRwCount<0>>,
  ) -> Self {
      EncryptionOracleDriver {
          process_grants,
          aes,
+         current_process: OptionalCell::empty(),
      }
  }

In practice, we simply want to find the next process request to work on. For this, we add a helper method to the impl of our EncryptionOracleDriver:

#![allow(unused)]
fn main() {
/// Return a `ProcessId` which has `request_pending` set, if there is some:
fn next_pending(&self) -> Option<ProcessId> {
    unimplemented!()
}
}

EXERCISE: Try to implement this method according to its specification. If you're stuck, see whether the documentation of the OptionalCell and Grant types help. Hint: to interact with the ProcessState of every processes, you can use the iter method on a Grant: the returned Iter type then has an enter method access the contents of an invidiual process' grant.

CHECKPOINT: encryption_oracle_chkpt3.rs

Interacting with Process Buffers and Scheduling Upcalls

For our encryption oracle, it is important to allow users provide buffers containing the encryption initialization vector (to prevent an attacker from inferring relationships between messages encrypted with the same key), and the plaintext or ciphertext to encrypt and decrypt respectively. Furthermore, userspace must provide a mutable buffer for our capsule to write the operation's output to. These buffers are placed into read-only and read-write allow slots by applications accordingly. We allocate fixed IDs for those buffers:

#![allow(unused)]
fn main() {
/// Ids for read-only allow buffers
mod ro_allow {
    pub const IV: usize = 0;
    pub const SOURCE: usize = 1;
    /// The number of allow buffers the kernel stores for this grant
    pub const COUNT: u8 = 2;
}

/// Ids for read-write allow buffers
mod rw_allow {
    pub const DEST: usize = 0;
    /// The number of allow buffers the kernel stores for this grant
    pub const COUNT: u8 = 1;
}
}

To deliver upcalls to the application, we further allocate an allow-slot for the DONE callback:

#![allow(unused)]
fn main() {
/// Ids for subscribe upcalls
mod upcall {
    pub const DONE: usize = 0;
    /// The number of subscribe upcalls the kernel stores for this grant
    pub const COUNT: u8 = 1;
}
}

Now, we need to update our Grant type to actually reserve these new allow and subscribe slots:

  pub struct EncryptionOracleDriver<'a, A: AES128<'a> + AES128Ctr> {
      aes: &'a A,
      process_grants: Grant<
          ProcessState,
-         UpcallCount<0>,
-         AllowRoCount<0>,
-         AllowRwCount<0>,
+         UpcallCount<{ upcall::COUNT }>,
+         AllowRoCount<{ ro_allow::COUNT }>,
+         AllowRwCount<{ rw_allow::COUNT }>,

      >,

Update this type signature in your constructor as well.

While Tock applications can expose certain sections of their memory as buffers to the kernel, access to the buffers is limited while their grant region is entered (implemented through a Rust closure). Unfortunately, this implies that asynchronous operations cannot keep a hold of these buffers and use them while other code (or potentially the application itself) is executing.

For this reason, Tock uses static mutable slices (&'static mut [u8]) in HILs. These Rust types have the distinct advantage that they can be passed around the kernel as "persistent references": when borrowing a 'static reference into another 'static reference, the original reference becomes inaccessible. Tock features a special container to hold such mutable references, called TakeCell. We add such a container for each of our source and destination buffers:

  use core::cell::Cell;
  use kernel::utilities::cells::TakeCell;

  pub struct EncryptionOracleDriver<'a, A: AES128<'a> + AES128Ctr> {
      [...],
	  current_process: OptionalCell<ProcessId>,
+     source_buffer: TakeCell<'static, [u8]>,
+     dest_buffer: TakeCell<'static, [u8]>,
+     crypt_len: Cell<usize>,
  }
  ) -> Self {
      EncryptionOracleDriver {
          process_grants: process_grants,
          aes: aes,
          current_process: OptionalCell::empty(),
+         source_buffer: TakeCell::new(source_buffer),
+         dest_buffer: TakeCell::new(dest_buffer),
+         crypt_len: Cell::new(0),
      }
  }

Now we have all pieces in place to actually drive the AES implementation. As this is a rather lengthy implementation containing a lot of specifics relating to the AES128 trait, this logic is provided to you in the form of a single run() method. Fill in this implementation from encryption_oracle_chkpt4.rs:

#![allow(unused)]
fn main() {
use kernel::processbuffer::ReadableProcessBuffer;
use kernel::hil::symmetric_encryption::AES128_BLOCK_SIZE;

/// The run method initiates a new decryption operation or
/// continues an existing two-phase (asynchronous) decryption in
/// the context of a process.
///
/// If the process-state `offset` is `0`, we will initialize the
/// AES engine with an initialization vector (IV) provided by the
/// application, and configure it to perform an AES128-CTR
/// operation.
///
/// If the process-state `offset` is larger or equal to the
/// process-provided source or destination buffer size, we return
/// an error of `ErrorCode::NOMEM`. A caller can use this as a
/// method to check whether the descryption operation has
/// finished.
fn run(&self, processid: ProcessId) -> Result<(), ErrorCode> {
    // Copy in the provided code from `encryption_oracle_chkpt4.rs`
    unimplemented!()
}
}

A core part still missing is actually invoking this run() method, namely for each process that has its request_pending flag set. As we need to do this each time an application requests an operation, as well as each time we finish an operation (to work on the next enqueued) one, this is implemented in a helper method called run_next_pending.

#![allow(unused)]
fn main() {
/// Try to run another decryption operation.
///
/// If `self.current_current` process contains a `ProcessId`, this
/// indicates that an operation is still in progress. In this
/// case, do nothing.
///
/// If `self.current_process` is vacant, use your implementation
/// of `next_pending` to find a process with an active request. If
/// one is found, remove its `request_pending` indication and start
//  a new decryption operation with the following call:
///
///    self.run(processid)
///
/// If this method returns an error, return the error to the
/// process in the registered upcall. Try this until either an
/// operation was started successfully, or no more processes have
/// pending requests.
///
/// Beware: you will need to enter a process' grant both to set the
/// `request_pending = false` and to (potentially) schedule an error
/// upcall. `self.run()` will itself also enter the grant region.
/// However, *Tock's grants are non-reentrant*. This means that trying
/// to enter a grant while it is already entered will fail!
fn run_next_pending(&self) {
    unimplemented!()
}
}

EXERCISE: Implement the run_next_pending method according to its specification. To schedule a process upcall, you can use the second argument passed into the grant.enter() method (kernel_data):

kernel_data.schedule_upcall(
    <upcall slot>,
    (<arg0>, <arg1>, <arg2>)
)

By convention, errors are reported in the first upcall argument (arg0). You can convert an ErrorCode into a usize with the following method:

kernel::errorcode::into_statuscode(<error code>)

run_next_pending should be invoked whenever we receive a new encryption / decryption request from a process, so add it to the command() method implementation:

  // Request the decryption operation:
- 1 => self
-     .process_grants
-     .enter(processid, |grant, _kernel_data| {
-         grant.request_pending = true;
-         CommandReturn::success()
-     })
-     .unwrap_or_else(|err| err.into()),
+ 1 => {
+     let res = self
+         .process_grants
+         .enter(processid, |grant, _kernel_data| {
+             grant.request_pending = true;
+             CommandReturn::success()
+         })
+         .unwrap_or_else(|err| err.into());
+
+     self.run_next_pending();
+
+     res
+ }

We store res temporarily, as Tock's grant regions are non-reentrant: we can't invoke run_next_pending (which will attempt to enter grant regions), while we're in a grant already.

CHECKPOINT: encryption_oracle_chkpt4.rs

Now, to complete our encryption oracle capsule, we need to implement the crypt_done() callback. This callback performs the following actions:

  • copies the in-kernel destination buffer (&'static mut [u8]) as passed to crypt() into the process' destination buffer through its grant, and
  • attempts to invoke another encryption / decryption round by calling run().
    • If calling run() succeeds, another crypt_done() callback will be scheduled in the future.
    • If calling run() fails with an error of ErrorCode::NOMEM, this indicates that the current operation has been completed. Invoke the process' upcall to signal this event, and use our run_next_pending() method to schedule the next operation.

Similar to the run() method, we provide this snippet to you in encryption_oracle_chkpt5.rs:

#![allow(unused)]
fn main() {
use kernel::processbuffer::WriteableProcessBuffer;

impl<'a, A: AES128<'a> + AES128Ctr> Client<'a> for EncryptionOracleDriver<'a, A> {
    fn crypt_done(&'a self, mut source: Option<&'static mut [u8]>, destination: &'static mut [u8]) {
	     // Copy in the provided code from `encryption_oracle_chkpt5.rs`
         unimplemented!()
    }
}
}

CHECKPOINT: encryption_oracle_chkpt5.rs

Congratulations! You have written your first Tock capsule and userspace driver, and interfaced with Tock's asynchronous HILs. Your capsule should be ready to go now, go ahead and integrate it into your HOTP application! Don't forget to recompile your kernel such that it integrates the latest changes.

Integrating the Encryption Oracle Capsule into your libtock-c App

The encryption oracle capsule is compatible with the oracle.c and oracle.h implementation in the libtock-c part of the tutorial, under examples/tutorials/hotp/hotp_oracle_complete/.

You can try to integrate this with your application by using the interfaces provided in oracle.h. The main.c file in this repository contains an example of how these interfaces can be integrated into a fully-featured HOTP application.