amboar.github.io

Software development notes

View My GitHub Profile

8 January 2020

Secure and Robust eMMC Flash Layout Design for BMCs

by Andrew

Recently a few of us were interested in designing an eMMC flash layout that allowed for secure boot of a BMC’s userspace while also catering to robustness across updates. This post covers a script I developed to road-test secure rootfs eMMC images under QEMU. The script appears at the end after the discussion of how we implement it.

Addressing the requirements, robustness across updates at least requires something akin to partitioning the storage into A and B devices for the rootfs, where rootfs updates are written to whichever partition isn’t the running session’s root. As for secure boot, the kernel needs some way to verify content from the root filesystem before executing it. Handily, Linux already has a couple of approaches to achieving this, one being the Integrity Measurement Architecture (IMA) and another being dm-verity. As a big handwave, IMA performs as a much finer-grain implementation of dm-verity, as dm-verity applies to an entire read-only filesystem on a block-basis.

As it turns out, read-only filesystems are fairly popular in embedded scenarios and the device-mapper functionality fits nicely with our requirement for robustness, so dm-verity was chosen as the way forward. We will be applying device-mapper and dm-verity to squashfses of the root filesystems.

The kernel’s device-mapper infrastructure essentially functions as a translation layer mapping logical to physical blocks on a storage device. This abstraction enables a whole host of behaviours to be transparently implemented in the kernel, such as disk encryption and RAID or testing and robustness concepts like unreliable IO. Our interests are rather more boring though, and we’ll just make use of the basic linear functionality to implement our partitions and dm-verity to provide security.

Working from the inside-out, lets get our squashfs rootfs set up with dm-verity. This is done via veritysetup, part of the cryptsetup repo. Here we meet a choice - veritysetup can store the metadata either appended to the data or in another device altogether. We’ll chose the simpler route and just append the metadata to the squashfs root to save configuring ourselves another device-mapper device. Assuming our rootfs is 7843840 bytes, we would issue:

$ veritysetup format \
     --hash sha256 \
     --data-block-size 4096 \
     --hash-block-size 4096 \
     --hash-offset 7843840 \
     rootfs.squashfs rootfs.squashfs

The data and hash block sizes in this case are in terms of the page size of the target system to avoid overhead in the kernel. The metadata must not become embedded in the last data block as this will cause an integrity check fail, so we must position the metadata as aligned on the block boundary subsequent to the end of the data. As it turns out, 7843840 mod 4096 = 0 so the align-up operation is a no-op in this example.

The veritysetup invocation outputs essential information such as the root hash and a randomly chosen salt value:

$ veritysetup format --hash sha256 --data-block-size 4096 --hash-block-size 4096 --hash-offset 7843840
VERITY header information for rootfs.squashfs
UUID:                   d3efa8ec-5668-4b8d-826e-1b2f1122cc5a
Hash type:              1
Data blocks:            1915
Data block size:        4096
Hash block size:        4096
Hash algorithm:         sha256
Salt:                   dddbe28bbd4b9def1fe48f69b912971cdae51295aea3afec5dcafae5ca9a8d1d
Root hash:              a68000e541d0980b89cd289d3b4325a1d1972ce90bbdcf8e434dbc2bfcf7e996

For secure-boot to function we must take the provided root hash, sign it with a private key recognised by the kernel, then inject the root hash, its signature and the salt value into the system’s boot environment.

A this point we’re finished with dm-verity, so lets now look at how to describe linear targets to the kernel. One way is to use an initrd and a script calling dmsetup. Another way is to describe the linear mapping table on the kernel commandline. Using the commandline approach requires fewer cogs in the machine, so this is the direction we will go.

Here’s an example that creates a linear device called a over a contiguous region at the start of /dev/mmcblk0, and lets say we want a device that exactly fits our dm-verity-protected image. With the dm-verity metadata appended to our rootfs it is now 7913472 bytes in size. Beware that the dm-mod.create offset and size parameters are described in terms of 512 byte sectors as opposed to the 4096 byte blocks used for veritysetup: We need to make sure our output image size is aligned-up to a multiple of both the block and sector sizes to avoid corruption. As it turns out 7913472 mod 4096 = 0 and 4096 mod 512 = 0 so we’re safe, and our 7913472 bytes represent 15456 sectors.

On the kernel commandline we can now create a linear device a for our verity-protected image. Using the construction parameters for the linear target:

Parameters: <dev path> <offset>
    <dev path>:
     Full pathname to the underlying block-device, or a
        "major:minor" device-number.
    <offset>:
     Starting sector within the device.

We put together the following:

dm-mod.create="a,,,rw, 0 15456 linear /dev/mmcblk0 0"

The creation of a b partition that takes on the role of accepting rootfs updates is a matter of defining another linear entry in the device-mapper table that doesn’t physically overlap with the blocks assigned to a on /dev/mmcblk0.

The device a device is exposed as /dev/dm-0 by the kernel. With that information at hand we can describe a dm-verity device nested in the linear device. Using the construction parameters for the verity target:

    <version> <dev> <hash_dev>
    <data_block_size> <hash_block_size>
    <num_data_blocks> <hash_start_block>
    <algorithm> <digest> <salt>
    [<#opt_params> <opt_params>]

And substituting the values in from the veritysetup output above we arrive at:

dm-mod.create="root,,,ro, 0 15320 verity 1 /dev/dm-0 /dev/dm-0 4096 4096 1915 1916 sha256 a68000e541d0980b89cd289d3b4325a1d1972ce90bbdcf8e434dbc2bfcf7e996 dddbe28bbd4b9def1fe48f69b912971cdae51295aea3afec5dcafae5ca9a8d1d"

Note that the hash start block value is one more than the number of data blocks, and the 15320 value represents the number of sectors occupied by the squashfs (i.e. excluding the verity metadata). /dev/dm-0 is listed for both the dev and hash_dev as we configured veritysetup to append the metadata to the data blocks.

Finally, to get the result to boot in QEMU we need to deal with some quirks of the MMC stack. QEMU assumes that the backing file for MMC storage devices is a multiple of a size recorded in the Card Specific Data register, and different MMC modes lead to varying expected sizes. The cheapest way out is to ensure the image size is a multiple of 512 kilobytes.

To tie that all together, here is a script to automate all the steps above and produce a QEMU commandline to boot the result under an ASPEED AST2600 EVB machine:

$ cat smash
#!/bin/sh

# SPDX-License-Identifier: Apache-2.0
# Copyright 2019 IBM Corp.

SRC_IMG=${SRC_IMG:-rootfs.squashfs}
DST_IMG=$(mktemp)
MMC_IMG=${MMC_IMG:-image.bin}

SECTOR_SIZE=512
BLOCK_SIZE=4096
CSD_SIZE=$((1 << (9 + 9 - 1 + 2)))

cleanup() {
	rm -f $DST_IMG
}

trap cleanup EXIT

align_up() {
	local offset=$1
	local size=$2

	echo $(((($offset + ($size - 1)) / $size) * $size))
}

verity_get_meta() {
	local needle="$1"
	local haystack="$2"

	echo "$haystack" | grep "$needle" | cut -d: -f2 | tr -d '[ \t]'
}

rm -f $MMC_IMG

dd if="$SRC_IMG" of="$DST_IMG" 2> /dev/null

VERITY_HASH_OFFSET=$(align_up $(stat --format=%s $SRC_IMG) $BLOCK_SIZE)
VERITY_HASH_BLOCKS=$(($VERITY_HASH_OFFSET / $BLOCK_SIZE))

VERITY_ALGO=sha256
VERITY_META="$(veritysetup format \
	--hash $VERITY_ALGO \
	--data-block-size $BLOCK_SIZE \
	--hash-block-size $BLOCK_SIZE \
	--hash-offset $VERITY_HASH_OFFSET \
	"$DST_IMG" "$DST_IMG")"

VERITY_SALT=$(verity_get_meta Salt "$VERITY_META")
VERITY_ROOT=$(verity_get_meta Root "$VERITY_META")

A_SECTORS=$(($(align_up $(stat --format=%s $DST_IMG) $BLOCK_SIZE) / $SECTOR_SIZE))
A_LINEAR="a,,,rw, 0 $A_SECTORS linear /dev/mmcblk0 0"
ROOT_SECTORS=$(($(align_up $(stat --format=%s $SRC_IMG) $BLOCK_SIZE) / $SECTOR_SIZE))
ROOT_VERITY="root,,,ro, 0 $ROOT_SECTORS verity 1 /dev/dm-0 /dev/dm-0 $BLOCK_SIZE $BLOCK_SIZE $VERITY_HASH_BLOCKS $(($VERITY_HASH_BLOCKS + 1)) $VERITY_ALGO $VERITY_ROOT $VERITY_SALT"

# Make an appropriately sized image for MMC
fallocate -l $(align_up $(stat --format=%s $DST_IMG) $CSD_SIZE) $MMC_IMG
dd if="$DST_IMG" of=$MMC_IMG conv=notrunc 2> /dev/null

echo export SMASH_DM_MOD_CREATE="'dm-mod.create=\"$A_LINEAR; $ROOT_VERITY\"'"
echo export SMASH_MMC_IMG=$MMC_IMG
echo

>&2 echo Example QEMU commandline:
>&2 echo
>&2 echo qemu-system-arm -M ast2600-evb -m 1024 -kernel zImage -dtb aspeed-ast2600-evb.dtb -nographic -drive file=sd1.img,if=sd,format=raw -drive file=sd2.img,if=sd,format=raw -drive file=\${SMASH_MMC_IMG},if=sd,format=raw -append '"console=ttyS4,1152008n earlyprintk debug $SMASH_DM_MOD_CREATE root=/dev/dm-1"'
tags: