diff --git a/Makefile b/Makefile index 24e35dc..33d85f1 100644 --- a/Makefile +++ b/Makefile @@ -9,13 +9,13 @@ .PHONY: check_mdbook check_mdbook_linkcheck check_mdbook_pagetoc check_prettier .PHONY: install_mdbook install_mdbook_linkcheck install_mdbook_pagetoc install_prettier -SHELL = /bin/bash +SHELL = /usr/bin/env bash # Configuration MDBOOK_VERSION := 0.4.32 MDBOOK_LINKCHECK_VERSION := 0.7.7 MDBOOK_PAGETOC_VERSION := 0.1.7 -PRETTIER_VERSION := 2.3.2 +PRETTIER_VERSION := 3.0.0 # For CI, use local install; for normal users system install # (already in PATH) is fine diff --git a/book.toml b/book.toml index a7a25ec..18f3426 100644 --- a/book.toml +++ b/book.toml @@ -10,3 +10,4 @@ additional-css = ["theme/pagetoc.css", "theme/pagetoc-tock.css"] additional-js = ["theme/pagetoc.js"] [output.linkcheck] +optional = true \ No newline at end of file diff --git a/dev-vm/README.md b/dev-vm/README.md new file mode 100644 index 0000000..a1d2adc --- /dev/null +++ b/dev-vm/README.md @@ -0,0 +1,15 @@ +# Packer-based Tutorial / Development VM Image + +Build instructions: + +1. Install VirtualBox and ensure that you can run a VM as an unprivileged user. +2. Install HashiCorp Packer. +3. Run `packer build packer.json`, and grab a cup of coffee. It'll download a + large Ubuntu server image, and then magically type some boot arguments. The + Ubuntu installer may take about 10 minutes, depending on your hardware and + network connection. +4. If it fails during installation because the installer crashed, there was + likely an intermittent network issue trying to download packages. Quit Packer + (`C-c`), wait until it finishes cleaning up, and then try again. +5. `mv output-virtualbox-iso tock-dev-vm` +6. `zip -r tock-dev-vm.zip tock-dev-vm`, avoid using .tar.\* for Windows diff --git a/dev-vm/install-jlink.sh b/dev-vm/install-jlink.sh new file mode 100644 index 0000000..518b586 --- /dev/null +++ b/dev-vm/install-jlink.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +JLINK_VERSION="766" +JLINK_URL="https://www.segger.com/downloads/jlink/JLink_Linux_V${JLINK_VERSION}_x86_64.deb" + +cat <", + "e", + "", + "", + "autoinstall ds=nocloud-net\\;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ ---", + "" + ], + "boot_wait": "1s", + + "cpus": 4, + "memory": 4096, + "disk_size": 131072, + + "guest_os_type": "Ubuntu_64", + "iso_checksum": "5e38b55d57d94ff029719342357325ed3bda38fa80054f9330dc789cd2d43931", + "iso_url": "http://releases.ubuntu.com/22.04.2/ubuntu-22.04.2-live-server-amd64.iso", + + "shutdown_command": "sudo shutdown -h now", + + "ssh_password": "tock", + "ssh_port": 22, + "ssh_read_write_timeout": "600s", + "ssh_timeout": "120m", + "ssh_username": "tock", + + "vboxmanage": [ + [ + "modifyvm", + "{{.Name}}", + "--nat-localhostreachable1", + "on", + "--vram", + "128", + "--graphicscontroller", + "vboxvga", + "--usb-ohci", + "on" + ] + ], + + "http_directory": "./" + } + ], + "post-processors": [ + ], + "provisioners": [ + { + "type": "file", + "source": "install-jlink.sh", + "destination": "/home/tock/install-jlink.sh" + }, + { + "type": "shell", + "script": "provision-vm.sh" + } + ] +} + diff --git a/dev-vm/provision-vm.sh b/dev-vm/provision-vm.sh new file mode 100644 index 0000000..b052a49 --- /dev/null +++ b/dev-vm/provision-vm.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +echo "Provisioning Tock development VM..." +cd /home/tock/ + +git clone https://github.com/tock/tock ./tock +git clone https://github.com/tock/libtock-c ./libtock-c +git clone https://github.com/tock/libtock-rs ./libtock-rs + +echo "Installing rustup" +curl https://sh.rustup.rs -sSf | sh -s -- -y + +echo "Installing Tockloader" +sudo pip3 install tockloader diff --git a/dev-vm/user-data b/dev-vm/user-data new file mode 100644 index 0000000..8ae63b5 --- /dev/null +++ b/dev-vm/user-data @@ -0,0 +1,126 @@ +#cloud-config + +# This is a modified version of [1], which installs automatically installs a +# Ubuntu server, converts it into a Ubuntu Desktop installation, and performs +# some further modifications to prepare it for the final provisioning stage +# in Packer. It already includes some packages which are going to be needed +# by the Tock provisioning script. +# +# [1]: https://github.com/canonical/autoinstall-desktop/blob/600a9ec2b9ef53a6945f11e227bbb680810ef6e3/autoinstall.yaml + +autoinstall: + # version is an Autoinstall required field. + version: 1 + + # The live-server ISO does not contain some of the required packages, + # such as ubuntu-desktop or the hwe kernel (or most of their depdendencies). + # The system being installed will need some sort of apt access. If you're + # behind a proxy, set that here: + # + # proxy: http://192.168.0.1:3142 + + # Add the ubuntu-desktop packages, along some other required or useful + # utilities: + packages: + - ubuntu-desktop + - ca-certificates + - cloud-guest-utils + - cloud-init + - curl + - e2fsprogs + - iproute2 + - openssh-server + - perl + - python3 + - python3-pip + - rsync + - sudo + - git + - vim + - htop + - nload + - tmux + + # This adds the default snaps found on a 22.04 Ubuntu Desktop system. + # Any desired additional snaps may also be listed here. + snaps: + - name: firefox + - name: gnome-3-38-2004 + - name: gtk-common-themes + - name: snap-store + - name: snapd-desktop-integration + + # Create a Tock user. It is given password-less sudo below: + identity: + realname: 'Tock Developer' + username: tock + # Password is 'tock' + password: '$6$nRD/3FTV3bz/J8jV$.PSxDdqbuwNIyDZcSSXVy/q4/Pe.M4ehABFrf/smpPKKlinEgi7WyI1Vp6IJz7O2ZGkXovwdF3uODLfrUacvx1' + hostname: tockvm + + # Install an OpenSSH server and ensure password-based login is enabled: + ssh: + install-server: yes + authorized-keys: [] + allow-pw: yes + + # Subiquity will, by default, configure a partition layout using LVM. + # The 'direct' layout method shown here will produce a non-LVM result. + storage: + layout: + name: direct + + # Ubuntu Desktop uses the hwe flavor kernel by default. + early-commands: + - echo 'linux-generic-hwe-22.04' > /run/kernel-meta-package + + late-commands: + # Enable the boot splash + - >- + curtin in-target -- + sed -i /etc/default/grub -e + 's/GRUB_CMDLINE_LINUX_DEFAULT=".*/GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"/' + - curtin in-target -- update-grub + + # Let NetworkManager handle network + - rm /target/etc/netplan/00-installer-config*yaml + - >- + printf "network:\n version: 2\n renderer: NetworkManager" + > /target/etc/netplan/01-network-manager-all.yaml + + # Remove default filesystem and related tools not used with the suggested + # 'direct' storage layout. These may yet be required if different + # partitioning schemes are used. + - >- + curtin in-target -- apt-get remove -y + btrfs-progs cryptsetup* lvm2 xfsprogs + + # Remove other packages present by default in Ubuntu Server but not + # normally present in Ubuntu Desktop. + - >- + curtin in-target -- apt-get remove -y + ubuntu-server ubuntu-server-minimal byobu dmeventd finalrd kpartx + mdadm ncurses-term needrestart open-iscsi sg3-utils ssh-import-id + sssd thin-provisioning-tools sosreport open-vm-tools + motd-news-config lxd-agent-loader landscape-common + + # Keep cloud-init, as it performs some of the installation on first boot. + - curtin in-target -- apt-get install -y cloud-init + + # Finally, remove things only installed as dependencies of other things + # we have already removed. + - curtin in-target -- apt-get autoremove -y + + # Enable password-less sudo for the tock user: + - "curtin in-target -- /bin/bash -c '\ + mkdir -p /etc/sudoers.d; \ + chmod 0755 /etc/sudoers.d; \ + echo \"tock ALL=(ALL) NOPASSWD: ALL\" > /etc/sudoers.d/tock; \ + chmod 0440 /etc/sudoers.d/tock; \ + chown -Rf root:root /etc/sudoers.d; \ + systemctl disable apt-daily.service; \ + systemctl disable apt-daily.timer; \ + systemctl disable apt-daily-upgrade.service; \ + systemctl disable apt-daily-upgrade.timer; \ + exit 0\ + '" diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 0ccfe4a..984b1db 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -12,10 +12,20 @@ Tock OS Course - [Course Setup](./course_setup.md) - [Modules](./modules.md) + - [USB Security Key](./key-overview.md) + - [Setup](./key-setup.md) + - [HOTP Application](./key-hotp-application.md) + - [Encryption Oracle Capsule](./key-hotp-oracle.md) + - [Access Control](./key-hotp-access.md) + - [Kernel Boot](./boot.md) + - [Policies](./policies.md) + - [TicKV](./tickv.md) + - [USB Keyboard](./usb-hid.md) - [Application](./application.md) + - [Graduation](./graduation.md) + - [Deprecated](./deprecated.md) - [Important Client](./important_client.md) - [Capsule](./capsule.md) - - [Graduation](./graduation.md) - [Mini Tutorials](./tutorials/tutorials.md) - [Blink an LED](./tutorials/01_running_blink.md) diff --git a/src/boot.md b/src/boot.md new file mode 100644 index 0000000..d4c48c8 --- /dev/null +++ b/src/boot.md @@ -0,0 +1,296 @@ +# Kernel Boot and Setup + +The goal of this module is to make you comfortable with the Tock kernel, how it +is structured, how the kernel is setup at boot, and how capsules provide +additional kernel functionality. + +During this you will: + +1. Learn how Tock uses Rust's memory safety to provide isolation for free +2. Read the Tock boot sequence, seeing how Tock uses static allocation +3. Learn about Tock's event-driven programming + +## The Tock Boot Sequence + +The _very_ first thing that runs on a Tock board is an assembly function called +`initialize_ram_jump_to_main()`. Rust requires that memory is configured before +any Rust code executes, so this must run first. As the function name implies, +control is then transferred to the `main()` function in the board's `main.rs` +file. Tock intentionally tries to give the board as much control over the +operation of the system as possible, hence why there is very little between +reset and the board's main function being called. + +Open the `main.rs` file for your board in your favorite editor. This file +defines the board's platform: how it boots, what capsules it uses, and what +system calls it supports for userland applications. + +### How is everything organized? + +Find the declaration of "platform" `struct`. This is typically called +`struct Platform` or named based on the name of the board (it's pretty early in +the file). This declares the structure representing the platform. It has many +fields, many of which are capsules that make up the board's platform. These +fields are resources that the board needs to maintain a reference to for future +use, for example for handling system calls or implementing kernel policies. + +Recall that everything in the kernel is statically allocated. We can see that +here. Every field in the platform `struct` is a reference to an object with a +static lifetime. + +Many capsules themselves take a lifetime as a parameter, which is currently +always `'static`. + +The boot process is primarily the construction of this platform structure. Once +everything is set up, the board will pass the constructed platform object to +`kernel::kernel_loop` and we're off to the races. + +### How do things get started? + +After RAM initialization, the reset handler invokes the `main()` function in the +board main.rs file. `main()` is typically rather long as it must setup and +configure all of the drivers and capsules the board needs. Many capsules depend +on other, lower layer abstractions that need to be created and initialized as +well. + +Take a look at the first few lines of `main()`. The boot sequence generally sets +up any low-level microcontroller configuration, initializes the MCU peripherals, +and sets up debugging capabilities. + +### How do capsules get created? + +The bulk of `main()` create and initializes capsules which provide the main +functionality of the Tock system. For example, to provide userspace applications +with ability to display serial data, boards typically create a `console` +capsule. An example of this looks like: + +```rust + +pub unsafe fn main() { + ... + + // Create a virtualizer on top of an underlying UART device. Use 115200 as + // the baud rate. + let uart_mux = components::console::UartMuxComponent::new(channel, 115200) + .finalize(components::uart_mux_component_static!()); + + // Instantiate the console capsule. This uses the virtualized UART provided + // by the uart_mux. + let console = components::console::ConsoleComponent::new( + board_kernel, + capsules_core::console::DRIVER_NUM, + uart_mux, + ) + .finalize(components::console_component_static!()); + + ... +} +``` + +Eventually, once all of the capsules have been created, we will populate the +platform structure with them: + +```rust +pub unsafe fn main() { + ... + + let platform = Platform { + console: console, + gpio: gpio, + ... + } + +} +``` + +#### What Are Components? + +When setting up the capsules (such as `console`), we used objects in the +`components` crate to help. In Tock, components are helper objects that make it +easier to correctly create and initialize capsules. + +For example, if we look under the hood of the `console` component, the main +initialization of console looks like: + +```rust +impl Component for ConsoleComponent { + fn finalize(self, s: Self::StaticInput) -> Console { + let grant_cap = create_capability!(capabilities::MemoryAllocationCapability); + + let write_buffer = static_init!([u8; DEFAULT_BUF_SIZE], [0; DEFAULT_BUF_SIZE]); + let read_buffer = static_init!([u8; DEFAULT_BUF_SIZE], [0; DEFAULT_BUF_SIZE]); + + let console_uart = static_init!( + UartDevice, + UartDevice::new(self.uart_mux, true) + ); + // Don't forget to call setup() to register our new UartDevice with the + // mux! + console_uart.setup(); + + let console = static_init!( + Console<'static>, + console::Console::new( + console_uart, + write_buffer, + read_buffer, + self.board_kernel.create_grant(self.driver_num, &grant_cap), + ) + ); + // Very easy to figure to set the client reference for callbacks! + hil::uart::Transmit::set_transmit_client(console_uart, console); + hil::uart::Receive::set_receive_client(console_uart, console); + + console + } +} +``` + +Much of the code within components is boilerplate that is copied for each board +and easy to subtlety miss an important setup step. Components encapsulate the +setup complexity and can be reused on each board Tock supports. + +The `static_init!` macro is simply an easy way to allocate a static variable +with a call to `new`. The first parameter is the type, the second is the +expression to produce an instance of the type. + +Components end up looking somewhat complex because they can be re-used across +multiple boards and different microcontrollers. More detail +[here](https://github.com/tock/tock/blob/master/kernel/src/component.rs). + +> #### A brief aside on buffers: +> +> Notice that the console needs both a read and write buffer for it to use. +> These buffers have to have a `'static` lifetime. This is because low-level +> hardware drivers, especially those that use DMA, require `'static` buffers. +> Since we don't know exactly when the underlying operation will complete, and +> we must promise that the buffer outlives the operation, we use the one +> lifetime that is assured to be alive at the end of an operation: `'static`. +> Other code with buffers without a `'static` lifetime, such as userspace +> processes, use capsules like `Console` by copying data into internal `'static` +> buffers before passing them to the console. The buffer passing architecture +> looks like this: +> +> ![Console/UART buffer lifetimes](imgs/console.svg) + +### Let's Make a Tock Board! + +The code continues on, creating all of the other capsules that are needed by the +platform. Towards the end of `main()`, we've created all of the capsules we +need, and it's time to create the actual platform structure +(`let platform = Platform {...}`). + +Boards must implement two traits to successfully run the Tock kernel: +`SyscallDriverLookup` and `KernelResources`. + +#### `SyscallDriverLookup` + +The first, `SyscallDriverLookup`, is how the kernel maps system calls from +userspace to the correct capsule within the kernel. The trait requires one +function: + +```rust +trait SyscallDriverLookup { + /// Mapping of syscall numbers to capsules. + fn with_driver(&self, driver_num: usize, f: F) -> R + where + F: FnOnce(Option<&dyn SyscallDriver>) -> R; +} +``` + +The `with_driver()` function executes the provided function `f()` by passing it +the correct capsule based on the provided `driver_num`. A brief example of an +implementation of `SyscallDriverLookup` looks like: + +```rust +impl SyscallDriverLookup for Platform { + fn with_driver(&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_core::gpio::DRIVER_NUM => f(Some(self.gpio)), + ... + _ => f(None), + } + } +} +``` + +Why require each board to provide this mapping? Why not implement this mapping +centrally in the kernel? Tock requires boards to implement this mapping as we +consider the assignment of driver numbers to specific capsules as a +platform-specific decisions. While Tock does have a default mapping of driver +numbers, boards are not obligated to use them. This flexibility allows boards to +expose multiple copies of the same capsule to userspace, for example. + +#### `KernelResources` + +The `KernelResources` trait is the main method for configuring the operation of +the core Tock kernel. Policies such as the syscall mapping described above, +syscall filtering, and watchdog timers are configured through this trait. More +information is contained in a separate course module. + +### Loading processes + +Once the platform is all set up, the board is responsible for loading processes +into memory: + +```rust +pub unsafe fn main() { + ... + + kernel::process::load_processes( + board_kernel, + chip, + core::slice::from_raw_parts( + &_sapps as *const u8, + &_eapps as *const u8 as usize - &_sapps as *const u8 as usize, + ), + core::slice::from_raw_parts_mut( + &mut _sappmem as *mut u8, + &_eappmem as *const u8 as usize - &_sappmem as *const u8 as usize, + ), + &mut PROCESSES, + &FAULT_RESPONSE, + &process_management_capability, + ) + .unwrap_or_else(|err| { + debug!("Error loading processes!"); + debug!("{:?}", err); + }); + + ... +} +``` + +A Tock process is represented by a `kernel::Process` struct. In principle, a +platform could load processes by any means. In practice, all existing platforms +write an array of Tock Binary Format (TBF) entries to flash. The kernel provides +the `load_processes` helper function that takes in a flash address and begins +iteratively parsing TBF entries and making `Process`es. + +> #### A brief aside on capabilities: +> +> To call `load_processes()`, the board had to provide a reference to a +> `&process_management_capability`. The `load_processes()` function internally +> requires significant direct access to memory, and it should only be called in +> very specific places. To prevent its misuse (for example from within a +> capsule), calling it requires a capability to be passed in with the arguments. +> To create a capability, the calling code must be able to call `unsafe`, Code +> (i.e. capsules) which cannot use `unsafe` therefore has no way to create a +> capability and therefore cannot call the restricted function. + +### Starting the kernel + +Finally, the board passes a reference to the current platform, the chip the +platform is built on (used for interrupt and power handling), and optionally an +IPC capsule to start the main kernel loop: + +```rust +board_kernel.kernel_loop(&platform, chip, Some(&platform.ipc), &main_loop_capability); + +``` + +From here, Tock is initialized, the kernel event loop takes over, and the system +enters steady state operation. diff --git a/src/capsule.md b/src/capsule.md index 8c2a52d..1ac3f4b 100644 --- a/src/capsule.md +++ b/src/capsule.md @@ -1,224 +1,4 @@ -# Capsule - -The goal of this part of the course is to make you comfortable with the Tock -kernel and writing code for it. By the end of this part, you'll have written a -new capsule that reads a humidity sensor and outputs its readings over the -serial port. - -During this you will: - -1. Learn how Tock uses Rust's memory safety to provide isolation for free -2. Read the Tock boot sequence, seeing how Tock uses static allocation -3. Learn about Tock's event-driven programming -4. Write a new capsule that reads a humidity sensor and prints it over serial - -## Read the Tock boot sequence (20m) - -Open `imix/src/main.rs` in your favorite editor. This file defines the imix -platform: how it boots, what capsules it uses, and what system calls it supports -for userland applications. - -### How is everything organized? - -Find the declaration of `struct Imix` (it's pretty early in the file). This -declares the structure representing the platform. It has many fields, all of -which are capsules. These are the capsules that make up the imix platform. For -the most part, these map directly to hardware peripherals, but there are -exceptions such as `IPC` (inter-process communication). - -Recall the discussion about how everything in the kernel is statically -allocated? We can see that here. Every field in `struct Imix` is a reference to -an object with a static lifetime. - -The capsules themselves take a lifetime as a parameter, which is currently -always `` `static``. The implementations of these capsules, however, do not rely -on this assumption. - -The boot process is primarily the construction of this `Imix` structure. Once -everything is set up, the board will pass the constructed `imix` to -`kernel::kernel_loop` and we're off to the races. - -### How do things get started? - -The method `reset_handler` is invoked when the chip resets (i.e., boots). It's -pretty long because imix has a lot of drivers that need to be created and -initialized, and many of them depend on other, lower layer abstractions that -need to be created and initialized as well. - -Take a look at the first few lines of the `reset_handler`. The boot sequence -initializes memory (copies initialized variables into RAM, clears the BSS), sets -up the system clocks, and configures the GPIO pins. - -### How do capsules get created? - -The next lines of `reset_handler` create and initialize the system console, -which is what turns calls to `println` into bytes sent to the USB serial port: - -```rust -let uart_mux = static_init!( - MuxUart<'static>, - MuxUart::new( - &sam4l::usart::USART3, - &mut capsules::virtual_uart::RX_BUF, - 115200 - ) -); -uart_mux.initialize(); - -hil::uart::Transmit::set_transmit_client(&sam4l::usart::USART3, uart_mux); -hil::uart::Receive::set_receive_client(&sam4l::usart::USART3, uart_mux); - -let console = ConsoleComponent::new(board_kernel, uart_mux).finalize(); -``` - -Eventually, once all of the capsules have been created, we will populate a imix -structure with them: - -```rust -let imix = Imix { - console: console, - gpio: gpio, - ... -``` - -The `static_init!` macro is simply an easy way to allocate a static variable -with a call to `new`. The first parameter is the type, the second is the -expression to produce an instance of the type. This call creates a `Console` -that uses serial port 3 (`USART3`) at 115200 bits per second. - -> #### A brief aside on buffers: -> -> Notice that you have to pass a write buffer to the console for it to use: this -> buffer has to have a `` `static`` lifetime. This is because low-level hardware -> drivers, especially those that use DMA, require `` `static`` buffers. Since -> Tock doesn't promise when a DMA operation will complete, and you need to be -> able to promise that the buffer outlives the operation, the one lifetime that -> is assured to be alive at the end of an operation is `` `static``. So that -> other code which has buffers without a `` `static`` lifetime, such as -> userspace processes, can use the `Console`, it copies them into its own -> internal `` `static`` buffer before passing it to the serial port. So the -> buffer passing architecture looks like this: -> -> ![Console/UART buffer lifetimes](imgs/console.svg) -> -> It's a little weird that Console's `new` method takes in a reference to -> itself. This is an ergonomics tradeoff. The Console needs a mutable static -> buffer to use internally, which the Console capsule declares. However writing -> global statics is unsafe. To avoid the unsafe operation in the Console capsule -> itself, we make it the responsibility of the instantiator to give the Console -> a buffer to use, without burdening the instantiator with sizing the buffer. - -### Let's make an imix! - -The code continues on, creating all of the other capsules that are needed by the -imix platform. By the time we get down to around line 360, we've created all of -the capsules we need, and it's time to create the actual imix platform structure -(`let imix = Imix {...}`). - -### Capsule _initialization_ - -Up to this point we have been creating numerous structures and setting some -static configuration options and mappings, but nothing dynamic has occurred -(said another way, all methods invoked by `static_init!` must be `const fn`, -however Tock's `static_init!` macro predates stabilization of `const fn`'s. A -future iteration could possibly leverage these and obviate the need for the -macro). - -Some capsules require _initialization_, some code that must be executed before -they can be used. For example, a few lines after creating the imix struct, we -initialize the console: - -```rust -imix.nrf51822.initialize(); -``` - -This method is responsible for actually writing the hardware registers that -configure the associated UART peripheral for use as a text console (8 data bits, -1 stop bit, no parity bit, no hardware flow control). - -### Inter-capsule dependencies - -Just after initializing the console capsule, we find this line: - -```rust -kernel::debug::assign_console_driver(Some(imix.console), kc); -``` - -This configures the kernel's `debug!` macro to print messages to this console -we've just created. The `debug!` mechanism can be very helpful during -development and testing. Today we're going to use it to print output from the -capsule you create. - -Let's try it out really quick: - -```diff ---- a/boards/imix/src/main.rs -+++ b/boards/imix/src/main.rs -@@ -10,7 +10,7 @@ - extern crate capsules; - extern crate cortexm4; - extern crate compiler_builtins; --#[macro_use(static_init)] -+#[macro_use(debug, static_init)] - extern crate kernel; - extern crate sam4l; - -@@ -388,6 +388,8 @@ pub unsafe fn reset_handler() { - capsules::console::App::default()); - kernel::debug::assign_console_driver(Some(imix.console), kc); - -+ debug!("Testing 1, 2, 3..."); -+ - imix.nrf51822.initialize(); -``` - -Compile and flash the kernel (`make program`) then look at the output -(`tockloader listen`). - -- What happens if you put the `debug!` before `assign_console_driver`? -- What happens if you put `imix.console.initialize()` after - `assign_console_driver`? - -As you can see, sometimes there are dependencies between capsules, and board -authors must take care during initialization to ensure correctness. - -> **Note:** The `debug!` implementation is _asynchronous_. It copies messages -> into a buffer and the console prints them via DMA as the UART peripheral is -> available, interleaved with other console users (i.e. processes). You -> shouldn't need to worry about the mechanics of this for now. - -### Loading processes - -Once the platform is all set up, the board is responsible for loading processes -into memory: - -```rust -kernel::process::load_processes(&_sapps as *const u8, - &mut APP_MEMORY, - &mut PROCESSES, - FAULT_RESPONSE); -``` - -A Tock process is represented by a `kernel::Process` struct. In principle, a -platform could load processes by any means. In practice, all existing platforms -write an array of Tock Binary Format (TBF) entries to flash. The kernel provides -the `load_processes` helper function that takes in a flash address and begins -iteratively parsing TBF entries and making `Process`es. - -### Starting the kernel - -Finally, the board passes a reference to the current platform, the chip the -platform is built on (used for interrupt and power handling), the processes to -run, and an IPC server instance to the main loop of the kernel: - -```rust -kernel::main(&imix, &mut chip, &mut PROCESSES, &imix.ipc); -``` - -From here, Tock is initialized, the kernel event loop takes over, and the system -enters steady state operation. - -### Create a "Hello World" capsule +# Create a "Hello World" capsule Now that you've seen how Tock initializes and uses capsules, you're going to write a new one. At the end of this section, your capsule will sample the @@ -246,7 +26,9 @@ debug!("Hello from the kernel!"); $ cd [PATH_TO_BOOK]/imix $ make program $ tockloader listen -No device name specified. Using default "tock" Using "/dev/ttyUSB0 - Imix IoT Module - TockOS" +No device name specified. +Using default "tock" +Using "/dev/ttyUSB0 - Imix IoT Module - TockOS" Listening for serial output. Hello from the kernel! ``` diff --git a/src/course.md b/src/course.md index 25ee304..96c381c 100644 --- a/src/course.md +++ b/src/course.md @@ -2,5 +2,5 @@ The Tock course includes several different modules that guide you through various aspects of Tock and Tock applications. Each module is designed to be -standalone such that a full course can be composed of different modules +fairly standalone such that a full course can be composed of different modules depending on the interests and backgrounds of those doing the course. diff --git a/src/course_setup.md b/src/course_setup.md index a47e7d8..00f9706 100644 --- a/src/course_setup.md +++ b/src/course_setup.md @@ -5,44 +5,124 @@ development setup and ensure you can communicate with the hardware. ## Hardware -![](imgs/imix.svg) - -The Tock course is currently written for an imix hardware platform. To follow -the directions directly, you will need an _imix_ hardware platform (pictured -above). Tock is a general operating system, however, and other boards _should_ -work, but they might not provide the exact same hardware sensors or peripherals. - -To complete the 6LoWPAN networking portion of this guide, you'll need an -additional imix to act as a hub. - -## Setup to Compile and Program the Kernel - -All of the hands-on exercises will be done within the source code for this book. -So pop open a terminal, and navigate to the repository. If you're using the VM, +Tock supports multiple different hardware platforms (called _boards_). These +boards differ in the chip they're using (Tock supports many ARM Cortex-M and +RISC-V chips), their on-board peripherals, and many other aspects. While Tock +attempts to be portable across many different platforms, not all boards are +equally well supported: some lack hardware to implement certain functionalities, +and others simply do not yet expose all of their peripherals through Tock +drivers and interfaces. Platforms can further differ in their available +resources, and how code-loading is implemented for them. + +Each of the modules in this course requires certain features to be present on +the board used. The modules will indicate a reference board for which they were +developed. For these boards, we provide instructions on how to program them +below. + +## Compile the Kernel + +All of the hands-on exercises will be done within the main Tock repository and +the `libtock-c` or `libtock-rs` userspace repositories. To work on the kernel, +pop open a terminal, and navigate to the repository. If you're using the VM, that'll be: - $ cd ~/book + $ cd ~/tock ### Make sure your Tock repository is up to date $ git pull +This will fetch the lastest commit from the Tock kernel repository. Individual +modules may ask you to check out specific commits or branches. In this case, be +sure to have those revisions checked out instead. + ### Build the kernel -To build the kernel, just type make in the `imix/` subdirectory. +To build the kernel for your board, navigate to the `boards/$YOUR_BOARD` +subdirectory. From within this subdirectory, a simple `make` should be +sufficient to build a kernel. For instance, for the Nordic nRF52840DK board, run +the following: - $ cd imix/ + $ cd boards/nordic/nrf52840dk $ make + Compiling nrf52840 v0.1.0 (/home/tock/tock/chips/nrf52840) + Compiling components v0.1.0 (/home/tock/tock/boards/components) + Compiling nrf52_components v0.1.0 (/home/tock/tock/boards/nordic/nrf52_components) + Finished release [optimized + debuginfo] target(s) in 24.07s + text data bss dec hex filename + 167940 4 28592 196536 2ffb8 /home/tock/tock/target/thumbv7em-none-eabi/release/nrf52840dk + 88302039a5698ab37d159ec494524cc466a0da2e9938940d2930d582404dc67a /home/tock/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin If this is the first time you are trying to make the kernel, the build system will use cargo and rustup to install various Tock dependencies. -If this is your first time building a Tock kernel for this particular -architecture, you may get an error complaining that you don't have the proper -the `cargo` target installed. We can use `rustup` to fix that: - - $ rustup target add thumbv7em-none-eabi - -> `imix` is based around an ARM Cortex-M4 microcontroller, which uses the -> thumbv7em instruction set. The rustup command above just downloads Rust core -> libraries for this architecture. +## Programming the kernel and interfacing with your board + +Boards may require slightly different procedures for programming the Tock +kernel. + +If you are following along with the provided VM, do not forget to pass your +board's USB interface(s) to the VM. In VirtualBox, this should work by selecting +"Devices > USB" and then enabling the respective device (for example "SEGGER +J-Link [0001]"). + +### Nordic nRF52840DK + +The Nordic nRF52840DK development board contains an integrated SEGGER J-Link +JTAG debugger, which can be used to program and debug the nRF52840 +microcontroller. It is also connected to the nRF's UART console and exposes this +as a console device. + +To flash the Tock kernel and applications through this interface, you will need +to have the SEGGER J-Link tools installed. If you are using a VM, we provide a +script you can execute to install these tools. **TODO!** + +With the J-Link software installed, we can use Tockloader to flash the Tock +kernel onto this board. Tockloader provides a flash command, which allows us to +write the kernel binary to a fixed location in the microcontroller's flash. The +nRF52840DK expects its kernel binary to be located at the beginning of flash +(indicated through `--address 0x00`). From the `nrf52840dk` board directory, run +the following command: + + $ tockloader flash --board nrf52dk --jlink --address 0x00 ../../../target/thumbv7em-none-eabi/release/nrf52840dk.bin + [INFO ] Using settings from KNOWN_BOARDS["nrf52dk"] + [STATUS ] Flashing binary to board... + [INFO ] Finished in 7.645 seconds + +Congrats! Tock should be running on your board now. + +To verify that Tock runs, try to connect to your nRF's serial console. +Tockloader provides a `tockloader listen` command for opening a serial +connection. In case you have multiple serial devices attached to your computer, +you may need to select the appropriate J-Link device: + + $ tockloader listen + [INFO ] No device name specified. Using default name "tock". + [INFO ] No serial port with device name "tock" found. + [INFO ] Found 2 serial ports. + Multiple serial port options found. Which would you like to use? + [0] /dev/ttyACM1 - J-Link - CDC + [1] /dev/ttyACM0 - L830-EB - Fibocom L830-EB + + Which option? [0] 0 + [INFO ] Using "/dev/ttyACM1 - J-Link - CDC". + [INFO ] Listening for serial output. + Initialization complete. Entering main loop + NRF52 HW INFO: Variant: AAC0, Part: N52840, Package: QI, Ram: K256, Flash: K1024 + tock$ + +In case you don't see any text printed after "Listening for serial output", try +hitting `[ENTER]` a few times. You should be greeted with a `tock$` shell +prompt. You can use the `reset` command to restart your nRF chip and see the +above greeting. + +In case you want to use a different serial console monitor, you may need to +identify the serial console device created for your board. On Linux, you can +identify the J-Link debugger's serial port by running: + + $ dmesg -Hw | grep tty + < ... some output ... > + < plug in the nRF52840DKs front USB (not "nRF USB") > + [ +0.003233] cdc_acm 1-3:1.0: ttyACM1: USB ACM device + +In this case, the nRF's serial console can be accessed as `/dev/ttyACM1`. diff --git a/src/deprecated.md b/src/deprecated.md new file mode 100644 index 0000000..37c50af --- /dev/null +++ b/src/deprecated.md @@ -0,0 +1,8 @@ +# Deprecated Course Modules + +These modules were previously developed but may not quite match the current Tock +code at this point. That is, the general ideas are still relevant and correct, +but the specific code might be somewhat outdated. + +We keep these for interested readers, but want to note that it might take a bit +more problem solving/updating to follow these steps than originally intended. diff --git a/src/imgs/encryption_oracle_capsule.svg b/src/imgs/encryption_oracle_capsule.svg new file mode 100644 index 0000000..156e7ee --- /dev/null +++ b/src/imgs/encryption_oracle_capsule.svg @@ -0,0 +1,781 @@ + + + + + + + + + + + + + + + + + + + Process A + + + + + + + Process B + + + + + + + + + + + Allow Source Buffer + + + + + + + Allo + w D + esti + nati + on + Buff + er + + + + + + + Su + b + sc + ri + be + + + Encryption Oracle Capsule + + + + + + + G + r + a + n + t + + + + + + + Source Buffer + + + + + + + (2) + + + crypt() + + + + + + + AES + + + Engine + + + + + + + (1) copy + + + + + + + + + + + + + Symmetric Encryption Client + + + + + + + + (3) + + + crypt_done() + + + + + + + (4) copy to grant + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (5) replace buffers + + + + + + + + + + + Destination Buffer + + + Command + + + (6) goto 1 + + + + or + + + + invoke upcall + + + + + + + + + + + Upcall + + + run() + + + + + diff --git a/src/imgs/usbkey.jpg b/src/imgs/usbkey.jpg new file mode 100644 index 0000000..a1d082e Binary files /dev/null and b/src/imgs/usbkey.jpg differ diff --git a/src/key-hotp-access.md b/src/key-hotp-access.md new file mode 100644 index 0000000..e512e25 --- /dev/null +++ b/src/key-hotp-access.md @@ -0,0 +1,411 @@ +# Security Key Application Access Control + +With security-focused and privileged system resources, a board may wish to +restrict which applications can access which system call resources. In this +module we will extend the Tock kernel to restrict access to the encryption +capsule to only trusted (credentialed) apps. + +## Background + +We need two Tock mechanisms to implement this feature. First, we need a way to +identify the trusted app that we will give access to the encryption engine. We +will do this by adding credentials to the app's +[TBF (Tock Binary Format file)](https://github.com/tock/tock/blob/master/doc/TockBinaryFormat.md) +and verifying those credentials when the application is loaded. This mechanism +allows developers to sign apps, and then the kernel can verify those signatures. + +The second mechanism is way to permit syscall access to only specific +applications. The Tock kernel already has a hook that runs on each syscall to +check if the syscall should be permitted. By default this just approves every +syscall. We will need to implement a custom policy which permits access to the +encryption capsule to only the trusted HOTP apps. + +## Module Overview + +Our goal is to add credentials to Tock apps, verify those credentials in the +kernel, and then permit only verified apps to use the encryption oracle API. To +keep this simple we will use a simple SHA-256 hash as our credential, and verify +that the hash is valid within the kernel. + +## Step 1: Credentialed Apps + +To implement our access control policy we need to include an offline-computed +SHA256 hash with the app TBF, and then check it when running the app. The SHA256 +credential is simple to create, and serves as a stand-in for more useful +credentials such as cryptographic signatures. + +This will require a couple pieces: + +- We need to actually include the hash in our app. +- We need a mechanism in the kernel to check the hash exists and is valid. + +### Signing Apps + +We can use Tockloader to add a hash to a compiled app. This will require +Tockloader version 1.10.0 or newer. + +First, compile the app: + +``` +$ cd libtock-c/examples/blink +$ make +``` + +Now, add the hash credential: + +``` +$ tockloader tbf credential add sha256 +``` + +It's fine to add to all architectures or you can specify which TBF to add it to. + +To check that the credential was added, we can inspect the TAB: + +``` +$ tockloader inspect-tab +``` + +You should see output like the following: + +``` +$ tockloader inspect-tab +[INFO ] No TABs passed to tockloader. +[STATUS ] Searching for TABs in subdirectories. +[INFO ] Using: ['./build/blink.tab'] +[STATUS ] Inspecting TABs... +TAB: blink + build-date: 2023-06-09 21:52:59+00:00 + minimum-tock-kernel-version: 2.0 + tab-version: 1 + included architectures: cortex-m0, cortex-m3, cortex-m4, cortex-m7 + + Which TBF to inspect further? cortex-m4 + +cortex-m4: + version : 2 + header_size : 104 0x68 + total_size : 16384 0x4000 + checksum : 0x722e64be + flags : 1 0x1 + enabled : Yes + sticky : No + TLV: Main (1) [0x10 ] + init_fn_offset : 41 0x29 + protected_size : 0 0x0 + minimum_ram_size : 5068 0x13cc + TLV: Program (9) [0x20 ] + init_fn_offset : 41 0x29 + protected_size : 0 0x0 + minimum_ram_size : 5068 0x13cc + binary_end_offset : 8360 0x20a8 + app_version : 0 0x0 + TLV: Package Name (3) [0x38 ] + package_name : blink + TLV: Kernel Version (8) [0x4c ] + kernel_major : 2 + kernel_minor : 0 + kernel version : ^2.0 + TLV: Persistent ACL (7) [0x54 ] + Write ID : 11 0xb + Read IDs (1) : 11 + Access IDs (1) : 11 + +TBF Footers + Footer + footer_size : 8024 0x1f58 + Footer TLV: Credentials (128) + Type: SHA256 (3) ✓ verified + Length: 32 + Footer TLV: Credentials (128) + Type: Reserved (0) + Length: 7976 +``` + +Note at the bottom, there is a `Footer TLV` with SHA256 credentials! Because +tockloader was able to double-check the hash was correct there is `✓ verified` +next to it. + +> **SUCCESS:** We now have an app with a hash credential! + +### Verifying Credentials in the Kernel + +To have the kernel check that our hash credential is present and valid, we need +to add a credential checker before the kernel starts each process. + +To create the app checker, we'll edit the board's `main.rs` file in the kernel. +Tock includes a basic SHA256 credential checker, so we can use that. The +following code should be added to the `main.rs` file somewhere before the +platform setup occurs (probably right after the encryption oracle capsule from +the last module!). + +```rust +//-------------------------------------------------------------------------- +// CREDENTIALS CHECKING POLICY +//-------------------------------------------------------------------------- + +// Create the software-based SHA engine. +let sha = static_init!(capsules_extra::sha256::Sha256Software<'static>, + capsules_extra::sha256::Sha256Software::new()); +kernel::deferred_call::DeferredCallClient::register(sha); + +// Create the credential checker. +static mut SHA256_CHECKER_BUF: [u8; 32] = [0; 32]; +let checker = static_init!( + kernel::process_checker::basic::AppCheckerSha256, + kernel::process_checker::basic::AppCheckerSha256::new(sha, &mut SHA256_CHECKER_BUF) + ); +kernel::hil::digest::Digest::set_client(sha, checker); +``` + +That code creates a `checker` object. We now need to modify the board so it +hangs on to that `checker` struct. To do so, we need to add this to our +`Platform` struct type definition near the top of the file: + +```rust +struct Platform { + ... + credentials_checking_policy: &'static kernel::process_checker::basic::AppCheckerSha256, +} +``` + +Then when we create the platform object near the end of `main()`, we can add our +`checker`: + +```rust +let platform = Platform { + ... + credentials_checking_policy: checker, +} +``` + +And we need the platform to provide access to that checker when requested by the +kernel for credentials-checking purposes. This goes in the `KernelResources` +implementation for the `Platform` type: + +```rust +impl KernelResources for Platform { + ... + type CredentialsCheckingPolicy = kernel::process_checker::basic::AppCheckerSha256; + ... + fn credentials_checking_policy(&self) -> &'static Self::CredentialsCheckingPolicy { + self.credentials_checking_policy + } + ... +} +``` + +Finally, we need to use the function that checks credentials when processes are +loaded (not just loads and executes them unconditionally). This should go at the +end of `main()`, replacing the existing call to +`kernel::process::load_processes`: + +```rust +kernel::process::load_and_check_processes( + board_kernel, + &platform, // note this function requires providing the platform. + chip, + core::slice::from_raw_parts( + &_sapps as *const u8, + &_eapps as *const u8 as usize - &_sapps as *const u8 as usize, + ), + core::slice::from_raw_parts_mut( + &mut _sappmem as *mut u8, + &_eappmem as *const u8 as usize - &_sappmem as *const u8 as usize, + ), + &mut PROCESSES, + &FAULT_RESPONSE, + &process_management_capability, + ) + .unwrap_or_else(|err| { + debug!("Error loading processes!"); + debug!("{:?}", err); + }); +``` + +Compile and install the updated kernel. + +> **SUCCESS:** We now have a kernel that can check credentials! + +### Installing Apps and Verifying Credentials + +Now, our kernel will only run an app if it has a valid SHA256 credential. To +verify this, recompile and install the blink app but do not add credentials: + +``` +cd libtock-c/examples/blink +touch main.c +make +tockloader install --erase +``` + +> **TODO** FIGURE OUT PROCESS CONSOLE ON TUTORIAL BOARD ISSUE!! + +Now, if we list the processes on the board with the process console. Note we +need to run the `console-start` command to active the tock process console. + +``` +$ tockloader listen +Initialization complete. Entering main loop +NRF52 HW INFO: Variant: AAF0, Part: N52840, Package: QI, Ram: K256, Flash: K1024 +console-start +tock$ +``` + +Now we can list the processes: + +``` +tock$ list + PID Name Quanta Syscalls Restarts Grants State + 0 blink 0 0 0 0/16 CredentialsFailed +tock$ +``` + +> Tip: You can re-disable the process console by using the `console-stop` +> command. + +You can see our app is in the state `CredentialsFailed` meaning it will not +execute (and the LEDs are not blinking). + +To fix this, we can add the SHA256 credential. + +``` +cd libtock-c/examples/blink +tockloader tbf credential add sha256 +tockloader install +``` + +Now when we list the processes, we see: + +``` +tock$ list + PID ShortID Name Quanta Syscalls Restarts Grants State + 0 0x3be6efaa blink 0 323 0 1/16 Yielded +``` + +And we can verify the app is both running and now has a specifically assigned +short ID. + +### Permitting Both Credentialed and Non-Credentialed Apps + +The default operation is not quite what we want. We want all apps to run, but +only credentialed apps to have access to the syscalls. + +To allow all apps to run, even if they don't pass the credential check, we need +to configure our checker. Doing that is actually quite simple. We just need to +modify the credential checker we are using to not require credentials. + +In `tock/kernel/src/process_checker/basic.rs`, modify the +`require_credentials()` function to not require credentials: + +```rust +impl AppCredentialsChecker<'static> for AppCheckerSha256 { + fn require_credentials(&self) -> bool { + false // change from true to false + } + ... +} +``` + +Then recompile and install. Now even a non-credentialed process should run: + +``` +tock$ list + PID ShortID Name Quanta Syscalls Restarts Grants State + 0 Unique c_hello 0 8 0 1/16 Yielded +``` + +> **SUCCESS:** We now can determine if an app is credentialed or not! + +## Step 2: Permitting Syscalls for only Credentialed Apps + +Our second step is to implement a policy that permits syscall access to the +encryption capsule only for credentialed apps. All other syscalls should be +permitted. + +Tock provides the `SyscallFilter` trait to do this. An object that implements +this trait is used on every syscall to check if that syscall should be executed +or not. By default all syscalls are permitted. + +The interface looks like this: + +```rust +pub trait SyscallFilter { + // Return Ok(()) to permit the syscall, and any Err() to deny. + fn filter_syscall( + &self, process: &dyn process::Process, syscall: &syscall::Syscall, + ) -> Result<(), errorcode::ErrorCode> { + Ok(()) + } +} + +``` + +We need to implement the single `filter_syscall()` function with out desired +behavior. + +To do this, create a new file in the board's `src/` directory. Then insert the +code below as a starting point: + +```rust +use kernel::platform::platform::SyscallFilter; +use kernel::process; +use kernel::process::Process; + +pub struct TrustedSyscallFilter {} + +impl SyscallFilter for TrustedSyscallFilter { + fn filter_syscall(&self, process: &dyn process::Process, syscall: &syscall::Syscall) + -> Result<(), errorcode::ErrorCode> { + + // To determine if the process has credentials we can use the + // `process.get_credentials()` function. + + // Now inspect the `syscall` the app is calling. If the `driver_numer` + // is not XXXXXX, then return `Ok(())` to permit the call. Otherwise, if + // the process is not credentialed, return `Err(ErrorCode::NOSUPPORT)`. If + // the process is credentialed return `Ok(())`. + + } +} +``` + +Documentation for the `Syscall` type is +[here](https://docs.tockos.org/kernel/syscall/enum.syscall). + +Save this file and include it from the board's main.rs. + +Now to put our new policy into effect we need to use it when we configure the +kernel via the `KernelResources` trait. + +```rust +impl KernelResources for Platform { + ... + type SyscallFilter = TrustedSyscallFilter; + ... + fn syscall_filter(&self) -> &'static Self::SyscallFilter { + self.syscall_filter + } + ... +} +``` + +Also you need to instantiate the `TrustedSyscallFilter` and add it to the +`Platform` struct. + +> **SUCCESS:** We now have a custom syscall filter based on app credentials. + +## Verifying HOTP Now Needs Credentials + +Now you should be able to install your HOTP app to the board without adding the +SHA256 credential and verify that it is no longer able to access the encryption +capsule. + +If you use tockloader to add credentials +(`tockloader tbf credential add sha256`) and then re-install your app it should +run as expected + +> ### Wrap-up +> +> You now have implemented access control on important kernel resources and +> enabled your app to use it. This provides platform builders robust flexibility +> in architecting the security framework for their devices. diff --git a/src/key-hotp-application.md b/src/key-hotp-application.md new file mode 100644 index 0000000..1c36358 --- /dev/null +++ b/src/key-hotp-application.md @@ -0,0 +1,279 @@ +# HOTP Application + +The motivating example for this entire tutorial is the creation of a USB +security key: a USB device that can be connected to your computer and +authenticate you to some service. One open standard for implementing such keys +is +[HMAC-based One-Time Password (HOTP)](https://en.wikipedia.org/wiki/HMAC-based_one-time_password). +It generates the 6 to 8 digit numeric codes which are used as a second-factor +for some websites. + +The crypto for implementing HOTP has already been created (HMAC and SHA256), so +you certainly don't have to be an expert in cryptography to make this +application work. We have actually implemented the software for generating HOTP +codes as well. Instead, you will focus on improving that code as a demonstration +of Tock and its features. + +On the application side, we'll start with a basic HOTP application which has a +pre-compiled HOTP secret key. Milestone one will be improving that application +to take user input to reconfigure the HOTP secret. Milestone two will be adding +the ability to persistently store the HOTP information so it is remembered +across resets and power cycles. Finally, milestone three will be adding the +ability to handle multiple HOTP secrets simultaneously. + +The application doesn't just calculate HOTP codes, it implements a USB HID +device as well. This means that when plugged in through the proper USB port, it +appears as an additional keyboard to your computer and is capable of entering +text. + +We have provided starter code as well as completed code for each of the +milestones. If you're facing some bugs which are limiting your progress, you can +reference or even wholesale copy a milestone in order to advance to the next +parts of the tutorial. + +## Applications in Tock + +A few quick details on applications in Tock. + +Applications in Tock look much closer to applications on traditional OSes than +to normal 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. Importantly for later in this +tutorial, the kernel is really in full control here and can decide which +applications to run and what permissions they should be given. + +Applications make requests from the OS kernel through system calls, but for the +purposes of this part of the tutorial, those system calls are wrapped in calls +to driver libraries. The most important aspect though is that results from +system calls never interrupt a running application. The application must `yield` +to receive callbacks. Again, this is frequently hidden within synchronous +drivers, but our application code will have a `yield` in the main loop as well, +where it waits for button presses. + +The tool for interacting with Tock applications is called `Tockloader`. It is a +python package capable of loading applications onto a board, inspecting +applications on a board, modifying application binaries before they are loaded +on a board, and opening a console to communicate with running applications. +We'll reference various `Tockloader` commands which you'll run throughout the +tutorial. + +## Starter Code + +We'll start by playing around with the starter code which implements a basic +HOTP key. + +- Within the `libtock-c` checkout, navigate to + `libtock-c/examples/tutorials/hotp/hotp_starter/`. + + This contains the starter code for the HOTP application. It has a hardcoded + HOTP secret and generates an HOTP code from it each time the Button 1 on the + board is pressed. + +- To compile the application and load it onto your board, run `make flash` in + the terminal (running just `make` will compile but not upload). + + - You likely want to remove other applications that are running on your board + if there are any. You can see which applications are installed with + `tockloader list` and you can remove an app with `tockloader uninstall` (it + will let you choose which app(s) to remove). Bonus information: `make flash` + is just a shortcut for `make && tockloader install`. + +- To see console output from the application, run `tockloader listen` in a + separate terminal. + +> **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. + +- Since this application creates a USB HID device to enter HOTP codes, you'll + need a second USB cable which will connect directly to the microcontroller. + Plug it into the port on the left-hand side of the nRF52840DK labeled "nRF + USB". + + - After attaching the USB cable, you should restart the application by hitting + the reset button the nRF52840DK labeled "IF BOOT/RESET". + +- To generate an HOTP code, press "Button 1" on the nRF5240DK. You should see a + message printed to console output that says + `Counter: 0. Typed "750359" on the USB HID the keyboard`. + + The HOTP code will also be written out over the USB HID device. The six-digit + number should appear wherever your cursor is. + +- You can verify the HOTP values with + [https://www.verifyr.com/en/otp/check#hotp](https://www.verifyr.com/en/otp/check#hotp) + + Go to section "#2 Generate HOTP Code". Enter "test" as the HOTP Code to auth, + the current counter value from console as the Counter, "sha256" as the + Algorithm, and 6 as the Digits. Click "Generate" and you'll see a six-digit + HOTP code that should match the output of the Tock HOTP app. + +- The source code for this application is in the file `main.c`. + + This is roughly 300 lines of code and includes Button handling, HMAC use and + the HOTP state machine. Execution starts at the `main()` function at the + bottom of the file. + +- Play around with the app and take a look through the code to make sure it + makes sense. Don't worry too much about the HOTP next code generation, as it + already works and you won't have to modify it. + +> **Checkpoint**: You should be able to run the application and have it output + + HOTP codes over USB to your computer when Button 1 is pressed. + +## Milestone One: Configuring Secrets + +The first milestone is to modify the HOTP application to allow the user to set a +secret, rather than having a pre-compiled default secret. Completed code is +available in `hotp_milestone_one/` in case you run into issues. + +- First, modify the code in main() to detect when a user wants to change the + HOTP secret rather than get the next code. + + The simplest way to do this is to sense how long the button is held for. You + can delay a short period, roughly 500 ms would work well, and then read the + button again and check if it's still being pressed. You can wait synchronously + with the + [`delay_ms()` function](https://github.com/tock/libtock-c/blob/master/libtock/timer.h) + and you can read a button with the + [`button_read()` function](https://github.com/tock/libtock-c/blob/master/libtock/button.h). + + - Note that buttons are indexed from 0 in Tock. So "Button 1" on the hardware + is button number 0 in the application code. All four of the buttons on the + nRF52840DK are accessible, although the `initialize_buttons()` helper + function in main.c only initializes interrupts for button number 0. (You can + change this if you want!) + + - An alternative design would be to use different buttons for different + purposes. We'll focus on the first method, but feel free to implement this + however you think would work best. + +- For now, just print out a message when you detect the user's intent. Be sure + to compile and upload your modified application to test it. + +- Next, create a new helper function to allow for programming new secrets. This + function will have three parts: + + 1. The function should print a message about wanting input from the user. + + - Let them know that they've entered this mode and that they should type a + new HOTP secret. + + 2. The function should read input from the user to get the base32-encoded + secret. + + - You'll want to use the + [Console functions `getch()` and `putnstr()`](https://github.com/tock/libtock-c/blob/master/libtock/console.h). + `getch()` can read characters of user input while `putnstr()` can be used + to echo each character the user types. Make a loop that reads the + characters into a buffer. + + - Since the secret is in base32, special characters are not valid. The + easiest way to handle this is to check the input character with + [`isalnum()`](https://cplusplus.com/reference/cctype/isalnum/) and ignore + it if it isn't alphanumeric. + + - When the user hits the enter key, a `\n` character will be received. This + can be used to break from the loop. + + 3. The function should decode the secret and save it in the `hotp-key`. + + - Use the `program_default_secret()` implementation for guidance here. The + `default_secret` takes the place of the string you read from the user, + but otherwise the steps are the same. + +- Connect the two pieces of code you created to allow the user to enter a new + key. Then upload your code to test it! + + - You can test that the new secret works with + [https://www.verifyr.com/en/otp/check#hotp](https://www.verifyr.com/en/otp/check#hotp) + as described previously. + +> **Checkpoint**: Your HOTP application should now take in user-entered secrets +> and generate HOTP codes for them based on button presses. + +## Milestone Two: Persistent Secrets + +The second milestone is to save the HOTP struct in persistent Flash rather than +in volatile memory. After doing so, the secret and current counter values will +persist after resets and power outages. We'll do the saving to flash with the +App State driver, which allows an application to save some information to its +own Flash region in memory. Completed code is available in `hotp_milestone_two/` +in case you run into issues. + +- First, understand how the App State driver works by playing with some example + code. The App State test application is available in + [`libtock-c/examples/tests/app_state/main.c`](https://github.com/tock/libtock-c/blob/master/examples/tests/app_state/main.c) + + - Compile it and load it onto your board to try it out. + + - If you want to uninstall the HOTP application from the board, you can do so + with `tockloader uninstall`. When you're done, you can use that same command + to remove this application. + +- Next, we'll go back to the HOTP application code and add our own App State + implementation. + + Start by creating a new struct that holds both a `magic` field and the HOTP + key struct. + + - The value in the `magic` field can be any unique number that is unlikely to + occur by accident. A 32-bit value (that is neither all zeros nor all ones) + of your choosing is sufficient. + +- Create an App State initialization function that can be called from the start + of `main()` which will load the struct from Flash if it exists, or initialize + it and store it if it doesn't. + + - Be sure to call the initialization function _after_ the one-second delay at + the start of `main()` so that it doesn't attempt to modify Flash during + resets while uploading code. + +- Update code throughout your application to use the HOTP key inside of the App + State struct. + + You'll also need to synchronize the App State whenever part of the HOTP key is + modified: when programming a new secret or updating the counter. + +- Upload your code to test it. You should be able to keep the same secret and + counter value on resets and also on power cycles. + + - There is an on/off switch on the top left of the nRF52840DK you can use for + power cycling. + + - Note that uploading a modified version of the application _will_ overwrite + the App State and lose the existing values inside of it. + +> **Checkpoint:** Your application should now both allow for the configuring of +> HOTP secrets and the HOTP secret and counter should be persistent across +> reboots. + +## Milestone Three: Multiple HOTP Keys + +The third and final application milestone is to add multiple HOTP keys and a +method for choosing between them. This milestone is **optional**, as the rest of +the tutorial will work without it. If you're short on time, you can skip it +without issue. Completed code is available in `hotp_milestone_three/` in case +you run into issues. + +- The recommended implementation of multiple HOTP keys is to assign one key per + button (so four total for the nRF52840DK). A short press will advance the + counter and output the HOTP code while a long press will allow for + reprogramming of the HOTP secret. + +- The implementation here is totally up to you. Here are some suggestions to + consider: + + - Select which key you are using based on the button number of the most recent + press. You'll also need to enable interrupts for all of the buttons instead + of just Button 1. + + - Make the HOTP key in the App State struct into an array with up to four + slots. + + - Having multiple key slots allows for different numbers of digits for the + HOTP code on different slots, which you could experiment with. + +> **Checkpoint:** Your application should now hold multiple HOTP keys, each of +> which can be configured and is persistent across reboots. diff --git a/src/key-hotp-oracle.md b/src/key-hotp-oracle.md new file mode 100644 index 0000000..a8d9048 --- /dev/null +++ b/src/key-hotp-oracle.md @@ -0,0 +1,900 @@ +# 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](https://blog.includesecurity.com/2015/11/firmware-dumping-technique-for-an-arm-cortex-m0-soc/). +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](https://opentitan.org/), 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: + +```rust +// 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`: + +```diff + #[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](https://github.com/tock/tock/blob/master/doc/ExternalDependencies.md) +> 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](https://docs.tockos.org/kernel/syscall/trait.syscalldriver). + +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: + +```rust +#[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`](https://docs.tockos.org/kernel/grant/struct.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`: + +```rust +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`](https://docs.tockos.org/kernel/syscall/trait.syscalldriver#method.command): + this method is called whenever an application issues a _command_-type system + call towards this driver, and +- [`allocate_grant`](https://docs.tockos.org/kernel/syscall/trait.syscalldriver#tymethod.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. + +```rust +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](https://docs.tockos.org/kernel/syscall/struct.commandreturn). + +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: + +```rust +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: + +```rust +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: + +```diff ++ 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: + +```diff +- 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, 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: + +```diff +- 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: + +```rust +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: + +```diff + 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: + +```diff + 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: + +```diff + impl SyscallDriverLookup for Platform { + fn with_driver(&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](https://github.com/tock/tock/blob/master/doc/reference/trd3-hil-design.md). +HILs are organized as Rust modules, and can be found under the +[`kernel/src/hil/`](https://github.com/tock/tock/tree/master/kernel/src/hil) +directory. We will be working with the +[`symmetric_encryption.rs` HIL](https://github.com/tock/tock/blob/master/kernel/src/hil/symmetric_encryption.rs). + +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](https://github.com/tock/tock/blob/75af40ea947dfab3c1db57a04ca6273fde895a3a/kernel/src/hil/symmetric_encryption.rs#L84) +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](https://github.com/tock/tock/blob/75af40ea947dfab3c1db57a04ca6273fde895a3a/kernel/src/hil/symmetric_encryption.rs#L14), +on the _client_ registered with +[`set_client()`](https://github.com/tock/tock/blob/75af40ea947dfab3c1db57a04ca6273fde895a3a/kernel/src/hil/symmetric_encryption.rs#L31). + +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](imgs/encryption_oracle_capsule.svg) + +To allow our capsule to receive `crypt_done` callbacks, add the following trait +implementation: + +```rust +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`: + +```diff +- // 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`](https://docs.tockos.org/kernel/utilities/cells/struct.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. + +```diff + use kernel::utilities::cells::OptionalCell; + + pub struct EncryptionOracleDriver<'a, A: AES128<'a> + AES128Ctr> { + aes: &'a A, + process_grants: Grant, AllowRoCount<0>, AllowRwCount<0>>, ++ current_process: OptionalCell, + } +``` + +We need to add it to the constructor as well: + +```diff + pub fn new( + aes: &'a A, + _source_buffer: &'static mut [u8], + _dest_buffer: &'static mut [u8], + process_grants: Grant, 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`: + +```rust +/// Return a `ProcessId` which has `request_pending` set, if there is some: +fn next_pending(&self) -> Option { + unimplemented!() +} +``` + +> **EXERCISE:** Try to implement this method according to its specification. If +> you're stuck, see whether the documentation of the +> [`OptionalCell`](https://docs.tockos.org/kernel/utilities/cells/struct.optionalcell) +> and [`Grant`](https://docs.tockos.org/kernel/grant/struct.grant) types help. +> Hint: to interact with the `ProcessState` of every processes, you can use the +> [`iter` method on a `Grant`](https://docs.tockos.org/kernel/grant/struct.grant#method.iter): +> 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: + +```rust +/// 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: + +```rust +/// 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: + +```diff + 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: + +```diff + use core::cell::Cell; + use kernel::utilities::cells::TakeCell; + + pub struct EncryptionOracleDriver<'a, A: AES128<'a> + AES128Ctr> { + [...], + current_process: OptionalCell, ++ source_buffer: TakeCell<'static, [u8]>, ++ dest_buffer: TakeCell<'static, [u8]>, ++ crypt_len: Cell, + } +``` + +```diff + ) -> 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`: + +```rust +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`. + +```rust +/// 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](https://docs.tockos.org/kernel/grant/struct.grant#method.enter) +> (`kernel_data`): +> +> kernel_data.schedule_upcall( +> , +> (, , ) +> ) +> +> 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() + +`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: + +```diff + // 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`: + +```rust +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. diff --git a/src/key-overview.md b/src/key-overview.md new file mode 100644 index 0000000..623af50 --- /dev/null +++ b/src/key-overview.md @@ -0,0 +1,31 @@ +# Security USB Key with Tock + +This module and submodules will walk you through how to create a USB security +key using Tock. + +![Security Key](imgs/usbkey.jpg) + +## Hardware Notes + +To fully follow this guide you will need a hardware board that supports a +peripheral USB port (i.e. where the microcontroller has USB hardware support). +We recommend using the nRF52840dk. + +## Goal + +Our goal is to create a standards-compliant HOTP USB key that we can use with a +demo website. The key will support enrolling new URL domains and providing +secure authentication. + +The main logic of the key will be implemented as a userspace program. That +userspace app will use the kernel to decrypt the shared key for each domain, +send the HMAC output as a USB keyboard device, and store each encrypted key in a +nonvolatile key-value storage. + +## Stages + +This module is broken into three stages: + +1. [Creating an HOTP userspace application](./key-hotp-application.md). +2. [Creating an in-kernel encryption oracle](./key-hotp-oracle.md). +3. [Enforcing access control restrictions to the oracle](./key-hotp-access.md). diff --git a/src/key-setup.md b/src/key-setup.md new file mode 100644 index 0000000..01d9061 --- /dev/null +++ b/src/key-setup.md @@ -0,0 +1,57 @@ +# Security Key Setup + +## Hardware Setup + +First, you'll need an nRF52840DK board. Other boards that Tock supports should +work though, as long as they have at least one button. You'll also need two USB +cables, one for programming the board and the other for attaching it as a USB +device. + +There are a couple of configurations on the nRF52840DK board that you should +double-check. First, the "Power" switch on the top left should be set to "On". +Secondly, the "nRF power source" switch in the top middle of the board should be +set to "VDD". Finally, the "nRF ONLY | DEFAULT" switch on the bottom right +should be set to "DEFAULT". + +For now, you should plug one USB cable into the top of the board for programming +(NOT into the "nRF USB" port on the side). We'll attach the other USB cable +later. + +## Software Setup + +If you followed the previous ["Course Setup"](course_setup.md) steps and/or the +["Getting Started" guide](https://github.com/tock/tock/blob/master/doc/Getting_Started.md) +you should have the software you need. + +As a reminder though, you'll need local clones of the +[Tock repo](https://github.com/tock/tock) and the +[Libtock-C repo](https://github.com/tock/libtock-c), which hold the kernel and +C-userland applications respectively. To compile code, you'll need the Rust +toolchain and the GCC embedded C toolchain. To upload code to the board you'll +need Tockloader, a python tool created to interact with Tock boards. Finally, +you'll want a couple of terminals and whatever code editor you prefer. + +## Programming the Kernel + +For the first part of the tutorial, you'll need the Tock kernel loaded onto the +nRF52840DK. We'll use a special version of the board that includes some code +we'll use for the tutorial which is located in `boards/nordic/nrf52840dk_demo`. + +`cd` into the `nrf52840dk_demo` directory, then to compile the kernel, you can +just type `make`. After that has completed, use `make flash` to upload the +kernel to your board. + +If everything worked properly, you should see a message that's something like +this: + +``` +$ make flash + Finished release [optimized + debuginfo] target(s) in 0.33s + text data bss dec hex filename + 172036 4 33100 205140 32154 /home/brghena/Dropbox/repos/tock/tock/target/thumbv7em-none-eabi/release/nrf52840dk +fbb724085db6dfd7530f792ffc50833108c5f26322f3ff9803363000439857c5 /home/brghena/Dropbox/repos/tock/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin +tockloader flash --address 0x00000 --board nrf52dk --jlink /home/brghena/Dropbox/repos/tock/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin +[INFO ] Using settings from KNOWN_BOARDS["nrf52dk"] +[STATUS ] Flashing binary to board... +[INFO ] Finished in 9.444 seconds +``` diff --git a/src/policies.md b/src/policies.md new file mode 100644 index 0000000..54fb9c4 --- /dev/null +++ b/src/policies.md @@ -0,0 +1,629 @@ +# Tock Kernel Policies + +As a kernel for a security-focused operating system, the Tock kernel is +responsible for implementing various policies on how the kernel should handle +processes. Examples of the types of questions these policies help answer are: +What happens when a process has a hardfault? Is the process restarted? What +syscalls are individual processes allowed to call? Which process should run +next? Different systems may need to answer these questions differently, and Tock +includes a robust platform for configuring each of these policies. + +## Background on Relevant Tock Design Details + +If you are new to this aspect of Tock, this section provides a quick primer on +the key aspects of Tock which make it possible to implement process policies. + +### The `KernelResources` Trait + +The central mechanism for configuring the Tock kernel is through the +`KernelResources` trait. Each board must implement `KernelResources` and provide +the implementation when starting the main kernel loop. + +The general structure of the `KernelResources` trait looks like this: + +```rust +/// This is the primary method for configuring the kernel for a specific board. +pub trait KernelResources { + /// How driver numbers are matched to drivers for system calls. + type SyscallDriverLookup: SyscallDriverLookup; + + /// System call filtering mechanism. + type SyscallFilter: SyscallFilter; + + /// Process fault handling mechanism. + type ProcessFault: ProcessFault; + + /// Credentials checking policy. + type CredentialsCheckingPolicy: CredentialsCheckingPolicy<'static> + 'static; + + /// Context switch callback handler. + type ContextSwitchCallback: ContextSwitchCallback; + + /// Scheduling algorithm for the kernel. + type Scheduler: Scheduler; + + /// Timer used to create the timeslices provided to processes. + type SchedulerTimer: scheduler_timer::SchedulerTimer; + + /// WatchDog timer used to monitor the running of the kernel. + type WatchDog: watchdog::WatchDog; + + // Getters for each policy/mechanism. + + fn syscall_driver_lookup(&self) -> &Self::SyscallDriverLookup; + fn syscall_filter(&self) -> &Self::SyscallFilter; + fn process_fault(&self) -> &Self::ProcessFault; + fn credentials_checking_policy(&self) -> &'static Self::CredentialsCheckingPolicy; + fn context_switch_callback(&self) -> &Self::ContextSwitchCallback; + fn scheduler(&self) -> &Self::Scheduler; + fn scheduler_timer(&self) -> &Self::SchedulerTimer; + fn watchdog(&self) -> &Self::WatchDog; +} +``` + +Many of these resources can be effectively no-ops by defining them to use the +`()` type. Every board that wants to support processes must provide: + +1. A `SyscallDriverLookup`, which maps the `DRIVERNUM` in system calls to the + appropriate driver in the kernel. +2. A `Scheduler`, which selects the next process to execute. The kernel provides + several common schedules a board can use, or boards can create their own. + +### Application Identifiers + +The Tock kernel can implement different policies based on different levels of +trust for a given app. For example, a trusted core app written by the board +owner may be granted full privileges, while a third-party app may be limited in +which system calls it can use or how many times it can fail and be restarted. + +To implement per-process policies, however, the kernel must be able to establish +a persistent identifier for a given process. To do this, Tock supports _process +credentials_ which are hashes, signatures, or other credentials attached to the +end of a process's binary image. With these credentials, the kernel can +cryptographically verify that a particular app is trusted. The kernel can then +establish a persistent identifier for the app based on its credentials. + +A specific process binary can be appended with zero or more credentials. The +per-board `KernelResources::CredentialsCheckingPolicy` then uses these +credentials to establish if the kernel should run this process and what +identifier it should have. The Tock kernel design does not impose any +restrictions on how applications or processes are identified. For example, it is +possible to use a SHA256 hash of the binary as an identifier, or a RSA4096 +signature as the identifier. As different use cases will want to use different +identifiers, Tock avoids specifying any constraints. + +However, long identifiers are difficult to use in software. To enable more +efficiently handling of application identifiers, Tock also includes mechanisms +for a per-process `ShortID` which is stored in 32 bits. This can be used +internally by the kernel to differentiate processes. As with long identifiers, +ShortIDs are set by `KernelResources::CredentialsCheckingPolicy` and are chosen +on a per-board basis. The only property the kernel enforces is that ShortIDs +must be unique among processes installed on the board. For boards that do not +need to use ShortIDs, the ShortID type includes a `LocallyUnique` option which +ensures the uniqueness invariant is upheld without the overhead of choosing +distinct, unique numbers for each process. + +```rust +pub enum ShortID { + LocallyUnique, + Fixed(core::num::NonZeroU32), +} +``` + +## Module Overview + +In this module, we are going to experiment with using the `KernelResources` +trait to implement per-process restart policies. We will create our own +`ProcessFaultPolicy` that implements different fault handling behavior based on +whether the process included a hash in its credentials footer. + +### Custom Process Fault Policy + +A process fault policy decides what the kernel does with a process when it +crashes (i.e. hardfaults). The policy is implemented as a Rust module that +implements the following trait: + +```rust +pub trait ProcessFaultPolicy { + /// `process` faulted, now decide what to do. + fn action(&self, process: &dyn Process) -> process::FaultAction; +} +``` + +When a process faults, the kernel will call the `action()` function and then +take the returned action on the faulted process. The available actions are: + +```rust +pub enum FaultAction { + /// Generate a `panic!()` with debugging information. + Panic, + /// Attempt to restart the process. + Restart, + /// Stop the process. + Stop, +} +``` + +Let's create a custom process fault policy that restarts signed processes up to +a configurable maximum number of times, and immediately stops unsigned +processes. + +We start by defining a `struct` for this policy: + +```rust +pub struct RestartTrustedAppsFaultPolicy { + /// Number of times to restart trusted apps. + threshold: usize, +} +``` + +We then create a constructor: + +```rust +impl RestartTrustedAppsFaultPolicy { + pub const fn new(threshold: usize) -> RestartTrustedAppsFaultPolicy { + RestartTrustedAppsFaultPolicy { threshold } + } +} +``` + +Now we can add a template implementation for the `ProcessFaultPolicy` trait: + +```rust +impl ProcessFaultPolicy for RestartTrustedAppsFaultPolicy { + fn action(&self, process: &dyn Process) -> process::FaultAction { + process::FaultAction::Stop + } +} +``` + +To determine if a process is trusted, we will use its `ShortID`. A `ShortID` is +a type as follows: + +```rust +pub enum ShortID { + /// No specific ID, just an abstract value we know is unique. + LocallyUnique, + /// Specific 32 bit ID number guaranteed to be unique. + Fixed(core::num::NonZeroU32), +} +``` + +If the app has a short ID of `ShortID::LocallyUnique` then it is untrusted (i.e. +the kernel could not validate its signature or it was not signed). If the app +has a concrete number as its short ID (i.e. `ShortID::Fixed(u32)`), then we +consider the app to be trusted. + +To determine how many times the process has already been restarted we can use +`process.get_restart_count()`. + +Putting this together, we have an outline for our custom policy: + +```rust +use kernel::process; +use kernel::process::Process; +use kernel::process::ProcessFaultPolicy; + +pub struct RestartTrustedAppsFaultPolicy { + /// Number of times to restart trusted apps. + threshold: usize, +} + +impl RestartTrustedAppsFaultPolicy { + pub const fn new(threshold: usize) -> RestartTrustedAppsFaultPolicy { + RestartTrustedAppsFaultPolicy { threshold } + } +} + +impl ProcessFaultPolicy for RestartTrustedAppsFaultPolicy { + fn action(&self, process: &dyn Process) -> process::FaultAction { + let restart_count = process.get_restart_count(); + let short_id = process.short_app_id(); + + // Check if the process is trusted. If so, return the restart action + // if the restart count is below the threshold. Otherwise return stop. + + // If the process is not trusted, return stop. + process::FaultAction::Stop + } +} +``` + +> **TASK:** Finish implementing the custom process fault policy. + +Save your completed custom fault policy in your board's `src/` directory as +`trusted_fault_policy.rs`. Then add `mod trusted_fault_policy;` to the top of +the board's `main.rs` file. + +### Testing Your Custom Fault Policy + +First we need to configure your kernel to use your new fault policy. + +1. Find where your `fault_policy` was already defined. Update it to use your new + policy: + + ```rust + let fault_policy = static_init!( + trusted_fault_policy::RestartTrustedAppsFaultPolicy, + trusted_fault_policy::RestartTrustedAppsFaultPolicy::new(3) + ); + ``` + +2. Now we need to configure the process loading mechanism to use this policy for + each app. + + ```rust + kernel::process::load_processes( + board_kernel, + chip, + flash, + memory, + &mut PROCESSES, + fault_policy, // this is where we provide our chosen policy + &process_management_capability, + ) + ``` + +3. Now we can compile the updated kernel and flash it to the board: + + ``` + # in your board directory: + make install + ``` + +Now we need an app to actually crash so we can observe its behavior. Tock has a +test app called `crash_dummy` that causes a hardfault when a button is pressed. +Compile that and load it on to the board: + +1. Compile the app: + + ``` + cd libtock-c/examples/tests/crash_dummy + make + ``` + +2. Install it on the board: + + ``` + tockloader install + ``` + +With the new kernel installed and the test app loaded, we can inspect the status +of the board. Use tockloader to connect to the serial port: + +``` +tockloader listen +``` + +> Note: if multiple serial port options appear, generally the lower numbered +> port is what you want to use. + +Now we can use the onboard console to inspect which processes we have on the +board. Run the list command: + +``` +tock$ list + PID Name Quanta Syscalls Restarts Grants State + 0 crash_dummy 0 6 0 1/15 Yielded +``` + +Note that `crash_dummy` is in the `Yielded` state. This means it is just waiting +for a button press. + +Press the first button on your board (it is "Button 1" on the nRF52840-dk). This +will cause the process to fault. You won't see any output, and since the app was +not signed it was just stopped. Now run the list command again: + +``` +tock$ list + PID Name Quanta Syscalls Restarts Grants State + 0 crash_dummy 0 6 0 0/15 Faulted +``` + +Now the process is in the `Faulted` state! This means the kernel will not try to +run it. Our policy is working! Next we have to verify signed apps so that we can +restart trusted apps. + +## App Credentials + +With our custom fault policy, we can implement different responses based on +whether an app is trusted or not. Now we need to configure the kernel to verify +apps, and check if we trust them or not. For this example we will use a simple +credential: a sha256 hash. This credential is simple to create, and serves as a +stand-in for more useful credentials such as cryptographic signatures. + +This will require a couple pieces: + +- We need to actually include the hash in our app. +- We need a mechanism in the kernel to check the hash exists and is valid. + +### Signing Apps + +We can use Tockloader to add a hash to a compiled app. + +First, compile the app: + +``` +$ cd libtock-c/examples/blink +$ make +``` + +Now, add the hash credential: + +``` +$ tockloader tbf credential add sha256 +``` + +It's fine to add to all architectures or you can specify which TBF to add it to. + +To check that the credential was added, we can inspect the TAB: + +``` +$ tockloader inspect-tab +``` + +You should see output like the following: + +``` +$ tockloader inspect-tab +[INFO ] No TABs passed to tockloader. +[STATUS ] Searching for TABs in subdirectories. +[INFO ] Using: ['./build/blink.tab'] +[STATUS ] Inspecting TABs... +TAB: blink + build-date: 2023-06-09 21:52:59+00:00 + minimum-tock-kernel-version: 2.0 + tab-version: 1 + included architectures: cortex-m0, cortex-m3, cortex-m4, cortex-m7 + + Which TBF to inspect further? cortex-m4 + +cortex-m4: + version : 2 + header_size : 104 0x68 + total_size : 16384 0x4000 + checksum : 0x722e64be + flags : 1 0x1 + enabled : Yes + sticky : No + TLV: Main (1) [0x10 ] + init_fn_offset : 41 0x29 + protected_size : 0 0x0 + minimum_ram_size : 5068 0x13cc + TLV: Program (9) [0x20 ] + init_fn_offset : 41 0x29 + protected_size : 0 0x0 + minimum_ram_size : 5068 0x13cc + binary_end_offset : 8360 0x20a8 + app_version : 0 0x0 + TLV: Package Name (3) [0x38 ] + package_name : kv_interactive + TLV: Kernel Version (8) [0x4c ] + kernel_major : 2 + kernel_minor : 0 + kernel version : ^2.0 + TLV: Persistent ACL (7) [0x54 ] + Write ID : 11 0xb + Read IDs (1) : 11 + Access IDs (1) : 11 + +TBF Footers + Footer + footer_size : 8024 0x1f58 + Footer TLV: Credentials (128) + Type: SHA256 (3) ✓ verified + Length: 32 + Footer TLV: Credentials (128) + Type: Reserved (0) + Length: 7976 +``` + +Note at the bottom, there is a `Footer TLV` with SHA256 credentials! Because +tockloader was able to double-check the hash was correct there is `✓ verified` +next to it. + +> **SUCCESS:** We now have an app with a hash credential! + +### Verifying Credentials in the Kernel + +To have the kernel check that our hash credential is present and valid, we need +to add a credential checker before the kernel starts each process. + +In `main.rs`, we need to create the app checker. Tock includes a basic SHA256 +credential checker, so we can use that: + +```rust +use capsules_extra::sha256::Sha256Software; +use kernel::process_checker::basic::AppCheckerSha256; + +// Create the software-based SHA engine. +let sha = static_init!(Sha256Software<'static>, Sha256Software::new()); +kernel::deferred_call::DeferredCallClient::register(sha); + +// Create the credential checker. +static mut SHA256_CHECKER_BUF: [u8; 32] = [0; 32]; +let checker = static_init!( + AppCheckerSha256, + AppCheckerSha256::new(sha, &mut SHA256_CHECKER_BUF) +); +sha.set_client(checker); +``` + +Then we need to add this to our `Platform` struct: + +```rust +struct Platform { + ... + credentials_checking_policy: &'static AppCheckerSha256, +} +``` + +Add it when create the platform object: + +```rust +let platform = Platform { + ... + credentials_checking_policy: checker, +} +``` + +And configure our kernel to use it: + +```rust +impl KernelResources for Platform { + ... + type CredentialsCheckingPolicy = AppCheckerSha256; + ... + fn credentials_checking_policy(&self) -> &'static Self::CredentialsCheckingPolicy { + self.credentials_checking_policy + } + ... +} +``` + +Finally, we need to use the function that checks credentials when processes are +loaded (not just loads and executes them unconditionally): + +```rust +kernel::process::load_and_check_processes( + board_kernel, + &platform, // note this function requires providing the platform. + chip, + core::slice::from_raw_parts( + &_sapps as *const u8, + &_eapps as *const u8 as usize - &_sapps as *const u8 as usize, + ), + core::slice::from_raw_parts_mut( + &mut _sappmem as *mut u8, + &_eappmem as *const u8 as usize - &_sappmem as *const u8 as usize, + ), + &mut PROCESSES, + &FAULT_RESPONSE, + &process_management_capability, + ) + .unwrap_or_else(|err| { + debug!("Error loading processes!"); + debug!("{:?}", err); + }); +``` + +(Instead of just `kernel::process::load_processes(...)`.) + +Compile and install the updated kernel. + +> **SUCCESS:** We now have a kernel that can check credentials! + +### Installing Apps and Verifying Credentials + +Now, our kernel will only run an app if it has a valid SHA256 credential. To +verify this, recompile and install the blink app but do not add credentials: + +``` +cd libtock-c/examples/blink +touch main.c +make +tockloader install --erase +``` + +Now, if we list the processes on the board with the process console: + +``` +$ tockloader listen +Initialization complete. Entering main loop +NRF52 HW INFO: Variant: AAF0, Part: N52840, Package: QI, Ram: K256, Flash: K1024 +tock$ list + PID Name Quanta Syscalls Restarts Grants State + 0 blink 0 0 0 0/16 CredentialsFailed +tock$ +``` + +You can see our app is in the state `CredentialsFailed` meaning it will not +execute (and the LEDs are not blinking). + +To fix this, we can add the SHA256 credential. + +``` +cd libtock-c/examples/blink +tockloader tbf credential add sha256 +tockloader install +``` + +Now when we list the processes, we see: + +``` +tock$ list + PID ShortID Name Quanta Syscalls Restarts Grants State + 0 0x3be6efaa blink 0 323 0 1/16 Yielded +``` + +And we can verify the app is both running and now has a specifically assigned +short ID. + +### Implementing the Privileged Behavior + +The default operation is not quite what we want. We want all apps to run, but +only credentialed apps to be restarted. + +First, we need to allow all apps to run, even if they don't pass the credential +check. Doing that is actually quite simple. We just need to modify the +credential checker we are using to not require credentials. + +In `tock/kernel/src/process_checker/basic.rs`, modify the +`require_credentials()` function to not require credentials: + +```rust +impl AppCredentialsChecker<'static> for AppCheckerSha256 { + fn require_credentials(&self) -> bool { + false // change from true to false + } + ... +} +``` + +Then recompile and install. Now both processes should run: + +``` +tock$ list + PID ShortID Name Quanta Syscalls Restarts Grants State + 0 0x3be6efaa blink 0 193 0 1/16 Yielded + 1 Unique c_hello 0 8 0 1/16 Yielded +``` + +But note, only the credential app (blink) has a specific short ID. + +Second, we need to use the presence of a specific short ID in our fault policy +to only restart credentials apps. We just need to check if the short ID is fixed +or not: + +```rust +impl ProcessFaultPolicy for RestartTrustedAppsFaultPolicy { + fn action(&self, process: &dyn Process) -> process::FaultAction { + let restart_count = process.get_restart_count(); + let short_id = process.short_app_id(); + + // Check if the process is trusted based on whether it has a fixed short + // ID. If so, return the restart action if the restart count is below + // the threshold. Otherwise return stop. + match short_id { + kernel::process::ShortID::LocallyUnique => process::FaultAction::Stop, + kernel::process::ShortID::Fixed(_) => { + if restart_count < self.threshold { + process::FaultAction::Restart + } else { + process::FaultAction::Stop + } + } + } + } +} +``` + +That's it! Now we have the full policy: we verify application credentials, and +handle process faults accordingly. + +> ### Task +> +> Compile and install multiple applications, including the crash dummy app, and +> verify that only credentialed apps are successfully restarted. + +> **SUCCESS:** We now have implemented an end-to-end security policy in Tock! diff --git a/src/tickv.md b/src/tickv.md new file mode 100644 index 0000000..ea1c09b --- /dev/null +++ b/src/tickv.md @@ -0,0 +1,296 @@ +# TicKV Key-Value Store + +[TicKV](https://github.com/tock/tock/tree/master/libraries/tickv) is a +flash-optimized key-value store written in Rust. Tock supports using TicKV +within the OS to enable the kernel _and_ processes to store and retrieve +key-value objects in local flash memory. + +## TicKV and Key-Value Design + +This section provides a quick overview of the TicKV and Key-Value stack in Tock. + +### TicKV Structure and Format + +TicKV can store 8 byte keys and values up to 2037 bytes. TicKV is page-based, +meaning that each object is stored entirely on a single page in flash. + +> Note: for familiarity, we use the term "page", but in actuality TicKV uses the +> size of the smallest _erasable_ region, not necessarily the actual size of a +> page in the flash memory. + +Each object is assigned to a page based on the lowest 16 bits of the key: + +```text +object_page_index = (key & 0xFFFF) % +``` + +Each object in TicKV has the following structure: + +```text +0 3 11 (bytes) +---------------------------------- ... - +| Header | Key | Value | +---------------------------------- ... - +``` + +The header has this structure: + +```text +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 (bits) +------------------------------------------------- +| Version=1 |V| res | Length | +------------------------------------------------- +``` + +- `Version`: Format of the object, currently this is always 1. +- `Valid (V)`: 1 if this object is valid, 0 otherwise. This is set to 0 to + delete an object. +- `Length (Len)`: The total length of the object, including the length of the + header (3 bytes), key (8 bytes), and value. + +Subsequent keys either start at the first byte of a page or immediately after +another object. If a key cannot fit on the page assigned by the +`object_page_index`, it is stored on the next page with sufficient room. + +Objects are updated in TicKV by invalidating the existing object (setting the +`V` flag to 0) and then writing the new value as a new object. This removes the +need to erase and re-write an entire page of flash to update a specific value. + +### TicKV on Tock Format + +The previous section describes the generic format of TicKV. Tock builds upon +this format by adding a header to the value buffer to add additional features. + +The full object format for TicKV objects in Tock has the following structure: + +```text +0 3 11 12 16 20 (bytes) +------------------------------------------------ ... ---- +| TicKV | Key |Ver| Length | Write | Value | +| Header | | | | ID | | +------------------------------------------------ ... ---- +<--TicKV Header+Key--><--Tock TicKV Header+Value-...----> +``` + +- `Version (Ver)`: One byte version of the Tock header. Currently 0. +- `Length`: Four byte length of the value. +- `Write ID`: Four byte identifier for restricting access to this object. + +The central addition is the `Write ID`, which is a `u32` indicating the +identifier of the writer that added the key-value object. The write ID of 0 is +reserved for the kernel to use. Each process can be assigned using TBF headers +its own write ID to use for storing state, such as in a TicKV database. Each +process and the kernel can then be granted specific read and update permissions, +based on the stored write ID. If a process has read permissions for the specific +ID stored in the `Write ID` field, then it can access that key-value object. If +a process has update permissions for the specific ID stored in the `Write ID` +field, then it can change the value of that key-value object. + +### Tock Key-Value APIs + +Tock supports two key-value orientated APIs: an upper and lower API. The lower +API expects hashed keys and is designed with flash as the underlying storage in +mind. The upper API is a more traditional K-V interface. + +The lower interface looks like this. Note, this version is simplified for +illustration, the actual version is complete Rust. + +```rust +pub trait KVSystem { + /// The type of the hashed key. For example `[u8; 8]`. + type K: KeyType; + + /// Create the hashed key. + fn generate_key(&self, unhashed_key: [u8], key: K) -> Result<(), (K, buffer,ErrorCode)>; + + /// Add a K-V object to the store. Error on collision. + fn append_key(&self, key: K, value: [u8]) -> Result<(), (K, buffer, ErrorCode)>; + + /// Retrieve a value from the store. + fn get_value(&self, key: K, value: [u8]) -> Result<(), (K, buffer, ErrorCode)>; + + /// Mark a K-V object as deleted. + fn invalidate_key(&self, key: K) -> Result<(), (K, ErrorCode)>; + + /// Cleanup the store. + fn garbage_collect(&self) -> Result<(), ErrorCode>; +} +``` + +(You can find the full definition in `tock/kernel/src/hil/kv_system.rs`.) + +In terms of TicKV, the `KVSystem` interface only uses the TicKV header. The Tock +header is only used in the upper level API. + +```rust +pub trait KVStore { + /// Get key-value object. + pub fn get(&self, key: [u8], value: [u8], perms: StoragePermissions) -> Result<(), (buffer, buffer, ErrorCode)>; + + /// Set or update a key-value object. + pub fn set(&self, key: [u8], value: [u8], perms: StoragePermissions) -> Result<(), (buffer, buffer, ErrorCode)>; + + /// Delete a key-value object. + pub fn delete(&self, key: [u8], perms: StoragePermissions) -> Result<(), (buffer, ErrorCode)>; +} +``` + +As you can see, each of these APIs requires a `StoragePermissions` so the +capsule can verify that the requestor has access to the given K-V object. + +## Key-Value in Userspace + +Userspace applications have access to the K-V store via the `kv_driver.rs` +capsule. This capsule provides an interface for applications to use the upper +layer get-set-delete API. + +However, applications need permission to use persistent storage. This is granted +via headers in the TBF header for the application. + +Applications have three fields for permissions: a write ID, multiple read IDs, +and multiple modify IDs. + +- `write_id: u32`: This u32 specifies the ID used when the application creates a + new K-V object. If this is 0, then the application does not have write access. + (A `write_id` of 0 is reserved for the kernel.) +- `read_ids: [u32]`: These read IDs specify which k-v objects the application + can call `get()` on. If this is empty or does not include the application's + `write_id`, then the application will not be able to retrieve its own objects. +- `modify_ids: [u32]`: These modify IDs specify which k-v objects the + application can edit, either by replacing or deleting. Again, if this is empty + or does not include the application's `write_id`, then the application will + not be able to update or delete its own objects. + +These headers can be added at compilation time with `elf2tab` or after the TAB +has been created using Tockloader. + +To have elf2tab add the header, it needs to be run with additional flags: + +``` +elf2tab ... --write_id 10 --read_ids 10,11,12 --access_ids 10,11,12 +``` + +To add it with tockloader (run in the app directory): + +``` +tockloader tbf tlv add persistent_acl 10 10,11,12 10,11,12 +``` + +### Using K-V Storage + +To use the K-V storage, load the kv-interactive app: + +``` +cd libtock-c/examples/tests/kv_interactive +make +tockloader tbf tlv add persistent_acl 10 10,11,12 10,11,12 +tockloader install +``` + +Now via the terminal, you can create and view k-v objects by typing `set`, +`get`, or `delete`. + +``` +$ tockloader listen +set mykey hello +Setting mykey=hello +Set key-value +get mykey +Getting mykey +Got value: hello +delete mykey +Deleting mykey +``` + +## Managing TicKV Database on your Host Computer + +You can interact with a board's k-v store via tockloader on your host computer. + +### View the Contents + +To view the entire DB: + +``` +tockloader tickv dump +``` + +Which should give something like: + +``` +[INFO ] Using jlink channel to communicate with the board. +[INFO ] Using settings from KNOWN_BOARDS["nrf52dk"] +[STATUS ] Dumping entire TicKV database... +[INFO ] Using settings from KNOWN_BOARDS["nrf52dk"] +[INFO ] Dumping entire contents of Tock-style TicKV database. +REGION 0 +TicKV Object hash=0xbbba2623865c92c0 version=1 flags=8 length=24 valid=True checksum=0xe83988e0 + Value: 00000000000b000000 + TockTicKV Object version=0 write_id=11 length=0 + Value: + +REGION 1 +TicKV Object hash=0x57b15d172140dec1 version=1 flags=8 length=28 valid=True checksum=0x32542292 + Value: 00040000000700000038313931 + TockTicKV Object version=0 write_id=7 length=4 + Value: 38313931 + +REGION 2 +TicKV Object hash=0x71a99997e4830ae2 version=1 flags=8 length=28 valid=True checksum=0xbdc01378 + Value: 000400000000000000000000ca + TockTicKV Object version=0 write_id=0 length=4 + Value: 000000ca + +REGION 3 +TicKV Object hash=0x3df8e4a919ddb323 version=1 flags=8 length=30 valid=True checksum=0x70121c6a + Value: 0006000000070000006b6579313233 + TockTicKV Object version=0 write_id=7 length=6 + Value: 6b6579313233 + +REGION 4 +TicKV Object hash=0x7bc9f7ff4f76f244 version=1 flags=8 length=15 valid=True checksum=0x1d7432bb + Value: +TicKV Object hash=0x9efe426e86d82864 version=1 flags=8 length=79 valid=True checksum=0xd2ac393f + Value: 001000000000000000a2a4a6a6a8aaacaec2c4c6c6c8caccce000000000000000000000000000000000000000000000000000000000000000000000000000000 + TockTicKV Object version=0 write_id=0 length=16 + Value: a2a4a6a6a8aaacaec2c4c6c6c8caccce + +REGION 5 +TicKV Object hash=0xa64cf33980ee8805 version=1 flags=8 length=29 valid=True checksum=0xa472da90 + Value: 0005000000070000006d796b6579 + TockTicKV Object version=0 write_id=7 length=5 + Value: 6d796b6579 + +REGION 6 +TicKV Object hash=0xf17b4d392287c6e6 version=1 flags=8 length=79 valid=True checksum=0x854d8de0 + Value: 00030000000700000033343500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + TockTicKV Object version=0 write_id=7 length=3 + Value: 333435 + +... + +[INFO ] Finished in 3.468 seconds +``` + +You can see all of the hashed keys and stored values, as well as their headers. + +### Add a Key-Value Object + +You can add a k-v object using tockloader: + +``` +tockloader tickv append newkey newvalue +``` + +Note that by default tockloader uses a `write_id` of 0, so that k-v object will +only be accessible to the kernel. To specify a specific `write_id` so an app can +access it: + +``` +tockloader tickv append appkey appvalue --write-id 10 +``` + +## Wrap-Up + +You now know how to use a Key-Value store in your Tock apps as well as in the +kernel. Tock's K-V stack supports access control on stored objects, and can be +used simultaneously by both the kernel and userspace applications. diff --git a/src/usb-hid.md b/src/usb-hid.md new file mode 100644 index 0000000..38e54fd --- /dev/null +++ b/src/usb-hid.md @@ -0,0 +1,44 @@ +# Implementing a USB Keyboard Device + +Our first task is to setup our kernel so that it is recognized as a USB keyboard +device. + +## Configuring the Kernel + +We need to setup our kernel to include USB support, and particularly the USB HID +(keyboard) profile. + +To do that, uncomment the USB code in `boards/nordic/nrf52840dk/src/main.rs`. + +**MORE HERE** + +Compile the kernel and load it on to your board. + +``` +cd tock/boards/nordic/nrf52840dk +make install +``` + +## Connecting the USB Device + +We will use both USB cables on our hardware. The main USB header is for +debugging and programming. The USB header connected directly to the +microcontroller will be the USB device. Ensure both USB devices are connected to +your computer. + +## Testing the USB Keyboard + +To test the USB keyboard device will will use a simple userspace application. +libtock-c includes an example app which just prints a string via USB keyboard +when a button is pressed. + +``` +cd libtock-c/examples/tests/keyboard_hid +make +tockloader install +``` + +Position your cursor somewhere benign, like a new terminal. Then press a button +on the board. + +> **Checkpoint:** You should see a welcome message from your hardware!