Summary

On Linux-based embedded systems implementing software authentication (secure boot and chain of trust), the file system verification is generally performed using an Initial RAM Filesystem (initramfs). Using an initramfs is more straight forward and flexible, as you can more easily adjust or calculate your verification arguments from the initramfs. For older kernels (< 5.4) which do not have in-kernel roothash signature verification, an initramfs is also essential if you wish to perform roothash signature verification from Linux during the boot sequence. However, as you might expect, using an initramfs may add undesirable boot time and storage requirements to the system. If your embedded system is operating with very strict boot timing or storage requirements, skipping the initramfs may be beneficial to you.  This can be done by using a technique called early device mapping.

 

Background Principles

The Linux kernel has a driver subsystem for device mapping, where a hardware storage device can be remapped through a target driver to produce a new, transparent, virtual storage device. This subsystem contains a few target drivers. One of these is called Verity, which is used for filesystem verification. Crypt is another one we commonly use, which provides filesystem encryption. We colloquially refer to these as DM-Verity and DM-Crypt. DM-Verity is what we will be using in this post. Let’s begin with a simple initramfs-based DM-Verity example.

Starting with an ext4 rootfs partition, we can generate the verity metadata from a build system via:

DATA_BLOCK_SIZE="4096"
HASH_BLOCK_SIZE="4096"
PART_SRC_EXT4=example.ext4
METADATA_HASH_DEV=example.ext4.metadata

veritysetup format --data-block-size=${DATA_BLOCK_SIZE} \
    --hash-block-size=${HASH_BLOCK_SIZE} \
    ${PART_SRC_EXT4} ${METADATA_HASH_DEV}

The output from this command looks like:

VERITY header information for example.ext4.metadata
UUID:               e17b33f3-ce02-4d9b-a0a8-90c85ebe3240
Hash type:          1
Data blocks:        16384
Data block size:    4096
Hash block size:    4096
Hash algorithm:     sha256
Salt:               2a4c7638f03b92bdb92d7284a742e0c4407c9ef65fdf2a7ea78ed02fde4a518b
Root hash:          b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941

Then when we mount and switch into the verified root filesystem from the initramfs during boot, we would do this from the target device:

MAPPER_NAME="verity_example"
PART_SRC_EXT4="/dev/mmcblk0p1"
METADATA_HASH_DEV="/dev/mmcblk0p2"
ROOTHASH="b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941"

veritysetup \
    --restart-on-corruption --ignore-zero-blocks \
    create ${MAPPER_NAME} ${PART_SRC_EXT4} \
    ${METADATA_HASH_DEV} ${ROOTHASH}

mkdir -p /newroot
mount /dev/mapper/${MAPPER_NAME} /newroot
switch_root /newroot /sbin/init

Now you’re done! Every block of data contained in /newroot will now verified by the kernel before it is used. However, part of the chain of trust is still missing from this example. To understand that, we’ll need to know what the DM-Verity root hash used above is. So, what is the root hash? It’s basically the last/top node of special a binary hash tree called a Merkle Tree. Each block of data is hashed and stored at the bottom level of this binary tree. Pairs of hashes are then subsequently hashed together until a final top hash is derived.

This Merkle Tree is also what’s contained inside the Verity metadata partition or file, along with some additional header information.

 

So, before running veritysetup to create the mountable Verity-backed /dev/mapper entry, the root hash needs to be verified too. Otherwise someone could break your chain of trust by replacing your entire root file system, metadata file system, and root hash input. You can verify this root hash from your bootloader via some form of trusted/secure boot like UEFI. However, an easy way to perform signature verification isn’t always available in many embedded bootloaders, so we opt to do it from within the kernel. Traditionally, this is done inside the initramfs using a library like OpenSSL.

So for example, from your build system, you could create an RSA-signed roothash via:

ROOTHASH="b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941"
PRIVATE_KEY="verity_private.pem"
PUBLIC_KEY="verity_public.pem"

#Generate RSA Key Pair
openssl genpkey -algorithm RSA -out ${PRIVATE_KEY} -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in ${PRIVATE_KEY} -out ${PUBLIC_KEY}

#Sign the root hash
echo ${ROOTHASH} | tr -d '\n' > roothash.txt
openssl rsautl -sign -inkey ${PRIVATE_KEY} -out ${ROOTHASH}.signed -in ${ROOTHASH}

Then on your embedded target, you would store the public key and signed root hash and check the authenticity via your initramfs:

#If this is correctly signed, it will return the roothash from the signed roothash file
ROOTHASH=$(openssl rsautl -verify -in "roothash.txt.signed" -inkey ${PUBLIC_KEY} -pubin)

#Result = b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941
echo ${ROOTHASH}

You can then use this resulting verified roothash to run “veritysetup create”, as shown above.

 

Linux Kernel-space Signature Verification

Signature verification in the Linux kernel is performed with CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y. This is a kernel configuration option which was added in 5.4. It adds the ability to do roothash signature verification in Linux’s kernel-space without any external tools or libraries.

This is utilized by creating an RSA certificate from the build system:

PRIVATE_KEY="verity_key.pem"
CERT="verity_cert.pem"
openssl req -x509 -newkey rsa:1024 -keyout ${PRIVATE_KEY} \
    -out ${CERT} -nodes -days 365 -set_serial 01 -subj /CN=example.com

Then you must add the certificate into your kernel build system with the appropriate config options:

CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y
CONFIG_SYSTEM_TRUSTED_KEYRING=y
CONFIG_SYSTEM_TRUSTED_KEYS="verity_cert.pem"

Then we create the signed root hash from the build system:

ROOTHASH="b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941"
PRIVATE_KEY="verity_key.pem"
CERT="verity_cert.pem"
echo ${ROOTHASH} | tr -d '\n' > roothash.txt
openssl smime -sign -nocerts -noattr -binary -in roothash.txt \
    -inkey ${PRIVATE_KEY} -signer ${CERT} -outform der -out roothash.txt.signed

Don’t forget to require signatures on the embedded target’s kernel command-line parameters by appending:

dm_verity.require_signatures=1

Then, finally, we can mount the file system from our initramfs:

MAPPER_NAME="verity_example"
PART_SRC_EXT4="/dev/mmcblk0p1"
METADATA_HASH_DEV="/dev/mmcblk0p2"
ROOTHASH="b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941"

veritysetup \
    --restart-on-corruption --ignore-zero-blocks --root-hash-signature=roothash.txt.signed \
    create "${MAPPER_NAME}" "${PART_SRC_EXT4}" \
    "${METADATA_HASH_DEV}" "${ROOTHASH}"

mkdir -p /newroot
mount /dev/mapper/${MAPPER_NAME} /newroot
switch_root /newroot /sbin/init

How can I do this without an initramfs? CONFIG_DM_INIT=y

CONFIG_DM_INIT is a kernel mechanism which allows you to pass a device mapper table to the kernel during boot, from the kernel command-line parameters.

So, from a command line perspective, the user-space command transforms from this:

veritysetup --ignore-zero-blocks \
    create "dm-0" "/dev/mmcblk0p1" "/dev/mmcblk0p2" \
    "b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941"

To this after we manually pass the DM table information into the kernel:

dm-mod.create="verity,,,ro,0 131072 verity 1 /dev/mmcblk0p1 /dev/mmcblk0p2 4096 4096 16384 1 sha256 \
               b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941 \
               2a4c7638f03b92bdb92d7284a742e0c4407c9ef65fdf2a7ea78ed02fde4a518b 1 ignore_zero_blocks"

To break this down a bit further:

dm-mod.create="<name>,<uuid>,<minor>,<flags>,[dm_table_params {dm_verity_params}]"

Where dm_table_params="<start_sector> <num_sectors> <target_type> <dm_verity_params>"

And dm_verity_params="<version> <dev> <hash_dev> <data_block_size> <hash_block_size> <num_data_blocks> \
                      <hash_start_block> <algorithm> <digest> <salt> [<#opt_params> <opt_params>]"

So, in our example:

name="verity"
uuid="unused/unset"
minor="unused/unset"
flags="ro"
dm_table_params=
    start_sector="0"
    num_sectors="131072"
    target_type="verity"
    target_args="dm_verity_params"
        version="1"
        dev="mmcblk0p1"
        hash_dev="mmcblk0p2"
        data_block_size="4096"
        hash_block_size="4096"
        num_data_blocks="16384"
        hash_start_block="1"
        algorithm="sha256"
        digest="b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941"
        salt="2a4c7638f03b92bdb92d7284a742e0c4407c9ef65fdf2a7ea78ed02fde4a518b"

From the “veritysetup format” command above, we can see how a lot of these parameters are derived:

VERITY header information for example.ext4.metadata
UUID:               e17b33f3-ce02-4d9b-a0a8-90c85ebe3240
Hash type:          1
Data blocks:        16384
Data block size:    4096
Hash block size:    4096
Hash algorithm:     sha256
Salt:               2a4c7638f03b92bdb92d7284a742e0c4407c9ef65fdf2a7ea78ed02fde4a518b
Root hash:          b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941

For the number of sectors, that is calculated via Verity Data Blocks * Verity Data Block Size / Sector Size. Assuming your sector size is 512 (most common), we then have 16384*4096/512 = 131072.

From U-Boot, I like to set all of this up similarly to:

setenv DATA_BLOCKS 16384
setenv DATA_BLOCK_SIZE 4096
setenv DATA_SECTORS 131072
setenv HASH_BLOCK_SIZE 4096
setenv HASH_ALG sha256
setenv SALT 2a4c7638f03b92bdb92d7284a742e0c4407c9ef65fdf2a7ea78ed02fde4a518b
setenv ROOT_HASH b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941
setenv DATA_DEV mmcblk0p1
setenv DATA_META_DEV mmcblk0p2

setenv bootargs ${bootargs} dm-mod.create="verity,,,ro,0 \${DATA_SECTORS} verity 1 /dev/\${DATA_DEV}
    /dev/\${DATA_META_DEV} \${DATA_BLOCK_SIZE} \${HASH_BLOCK_SIZE} \${DATA_BLOCKS} 1 \${HASH_ALG}
    \${ROOT_HASH} \${SALT} 1 ignore_zero_blocks" root=/dev/dm-0 dm_verity.require_signatures=1

Note: /dev/dm-0 is the first device mapper device that is created by this early device mapping procedure. If this is your rootfs, then you must also set root=/dev/dm-0, as we have above.

Also note: This demonstration was using a 1024 bit kernel RSA key which is able to fit into the lengthy U-Boot bootargs string. If you’re using 2048 or 4096 bits, you will most likely encounter problems with U-Boot’s max boot args length and need to increase it.

 

Wait, what about early root hash signature verification? Where did that go?

Good catch, we can’t use the OpenSSL+initramfs method anymore. Unfortunately, DM_VERITY_VERIFY_ROOTHASH_SIG also does not support early mapping, as it uses the kernel keyring system and does not provide a way to setup the keyring via the kernel command-line arguments.

Alright, fine, let’s fix it. We’ll add a new verity table parameter called “root_hash_sig_hex”, where we can set the incoming root hash signature, without the keyring.

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Wed, 30 Mar 2022 10:57:25 -0400
Subject: [PATCH 1/1] DM-Verity: Add root_hash_sig_hex parameter to early
 verity device mapping, so that we can pass a roothash signature in via
 /proc/cmdline.  This enables the ability to use DM_VERITY_VERIFY_ROOTHASH_SIG
 alongside early device mapping

---

 drivers/md/dm-verity-target.c     |  8 +++-
 drivers/md/dm-verity-verify-sig.c | 63 +++++++++++++++++++++++++++++++
 drivers/md/dm-verity-verify-sig.h | 19 +++++++++-
 3 files changed, 87 insertions(+), 3 deletions(-)

diff --git a/drivers/md/dm-verity-target.c b/drivers/md/dm-verity-target.c
index 3fb02167a590..047dd9a10264 100644
--- a/drivers/md/dm-verity-target.c
+++ b/drivers/md/dm-verity-target.c
@@ -930,7 +930,13 @@ static int verity_parse_opt_args(struct dm_arg_set *as, struct dm_verity *v,
             if (r)
                 return r;
             continue;
-
+        } else if (verity_verify_is_hex_sig_opt_arg(arg_name)) {
+            r = verity_verify_hex_sig_parse_opt_args(as, v,
+                                 verify_args,
+                                 &argc, arg_name);
+            if (r)
+                return r;
+            continue;
         }
 
         ti->error = "Unrecognized verity feature request";
diff --git a/drivers/md/dm-verity-verify-sig.c b/drivers/md/dm-verity-verify-sig.c
index 919154ae4cae..906e5a2034b5 100644
--- a/drivers/md/dm-verity-verify-sig.c
+++ b/drivers/md/dm-verity-verify-sig.c
@@ -28,6 +28,12 @@ bool verity_verify_is_sig_opt_arg(const char *arg_name)
                 DM_VERITY_ROOT_HASH_VERIFICATION_OPT_SIG_KEY));
 }
 
+bool verity_verify_is_hex_sig_opt_arg(const char *arg_name)
+{
+    return (!strcasecmp(arg_name,
+                DM_VERITY_ROOT_HASH_VERIFICATION_OPT_HEX_SIG_KEY));
+}
+
 static int verity_verify_get_sig_from_key(const char *key_desc,
                     struct dm_verity_sig_opts *sig_opts)
 {
@@ -64,6 +70,33 @@ static int verity_verify_get_sig_from_key(const char *key_desc,
     return ret;
 }
 
+static int verity_verify_get_sig_from_hex(const char *key_desc,
+                    struct dm_verity_sig_opts *sig_opts)
+{
+    int ret = 0, i = 0, j = 0;
+    uint8_t byte[3] = {0x00, 0x00, 0x00};
+    long result;
+
+    sig_opts->sig = kmalloc(strlen(key_desc)/2, GFP_KERNEL);
+    if (!sig_opts->sig) {
+        ret = -ENOMEM;
+        goto end;
+    }
+
+    sig_opts->sig_size = strlen(key_desc)/2;
+
+    for(i = 0, j = 0; i < strlen(key_desc)-1; i+=2, j+=1){
+        byte[0] = key_desc[i];
+        byte[1] = key_desc[i+1];
+        kstrtol(byte, 16, &result);
+        sig_opts->sig[j] = result;
+    }
+
+end:
+
+    return ret;
+}
+
 int verity_verify_sig_parse_opt_args(struct dm_arg_set *as,
                      struct dm_verity *v,
                      struct dm_verity_sig_opts *sig_opts,
@@ -93,6 +126,36 @@ int verity_verify_sig_parse_opt_args(struct dm_arg_set *as,
     return ret;
 }
 
+int verity_verify_hex_sig_parse_opt_args(struct dm_arg_set *as,
+                     struct dm_verity *v,
+                     struct dm_verity_sig_opts *sig_opts,
+                     unsigned int *argc,
+                     const char *arg_name)
+{
+    struct dm_target *ti = v->ti;
+    int ret = 0;
+    const char *sig_key = NULL;
+
+    if (!*argc) {
+        ti->error = DM_VERITY_VERIFY_ERR("Signature key not specified");
+        return -EINVAL;
+    }
+
+    sig_key = dm_shift_arg(as);
+    (*argc)--;
+
+    ret = verity_verify_get_sig_from_hex(sig_key, sig_opts);
+    if (ret < 0)
+        ti->error = DM_VERITY_VERIFY_ERR("Invalid key specified");
+
+    v->signature_key_desc = kstrdup(sig_key, GFP_KERNEL);
+    if (!v->signature_key_desc)
+        return -ENOMEM;
+
+    return ret;
+}
+
+
 /*
  * verify_verify_roothash - Verify the root hash of the verity hash device
  *                 using builtin trusted keys.
diff --git a/drivers/md/dm-verity-verify-sig.h b/drivers/md/dm-verity-verify-sig.h
index 19b1547aa741..2be00114becc 100644
--- a/drivers/md/dm-verity-verify-sig.h
+++ b/drivers/md/dm-verity-verify-sig.h
@@ -10,6 +10,7 @@
 
 #define DM_VERITY_ROOT_HASH_VERIFICATION "DM Verity Sig Verification"
 #define DM_VERITY_ROOT_HASH_VERIFICATION_OPT_SIG_KEY "root_hash_sig_key_desc"
+#define DM_VERITY_ROOT_HASH_VERIFICATION_OPT_HEX_SIG_KEY "root_hash_sig_hex"
 
 struct dm_verity_sig_opts {
     unsigned int sig_size;
@@ -23,11 +24,13 @@ struct dm_verity_sig_opts {
 int verity_verify_root_hash(const void *data, size_t data_len,
                 const void *sig_data, size_t sig_len);
 bool verity_verify_is_sig_opt_arg(const char *arg_name);
-
+bool verity_verify_is_hex_sig_opt_arg(const char *arg_name);
 int verity_verify_sig_parse_opt_args(struct dm_arg_set *as, struct dm_verity *v,
                     struct dm_verity_sig_opts *sig_opts,
                     unsigned int *argc, const char *arg_name);
-
+int verity_verify_hex_sig_parse_opt_args(struct dm_arg_set *as, struct dm_verity *v,
+                    struct dm_verity_sig_opts *sig_opts,
+                    unsigned int *argc, const char *arg_name);
 void verity_verify_sig_opts_cleanup(struct dm_verity_sig_opts *sig_opts);
 
 #else
@@ -45,6 +48,11 @@ bool verity_verify_is_sig_opt_arg(const char *arg_name)
     return false;
 }
 
+bool verity_verify_is_hex_sig_opt_arg(const char *arg_name)
+{
+    return false;
+}
+
 int verity_verify_sig_parse_opt_args(struct dm_arg_set *as, struct dm_verity *v,
                     struct dm_verity_sig_opts *sig_opts,
                     unsigned int *argc, const char *arg_name)
@@ -52,6 +60,13 @@ int verity_verify_sig_parse_opt_args(struct dm_arg_set *as, struct dm_verity *v,
     return -EINVAL;
 }
 
+int verity_verify_hex_sig_parse_opt_args(struct dm_arg_set *as, struct dm_verity *v,
+                    struct dm_verity_sig_opts *sig_opts,
+                    unsigned int *argc, const char *arg_name)
+{
+    return -EINVAL;
+}
+
 void verity_verify_sig_opts_cleanup(struct dm_verity_sig_opts *sig_opts)
 {
 }

So, this patch allows the kernel to take the roothash signature in as a hex-formatted string from the command-line arguments and converts it back into raw data when it is copied into sig_opts->sig. This is done inside the verity_verify_get_sig_from_hex() section of the patch.

Now that our kernel is patched, we can do the following from our build system:

ROOTHASH="b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941"
PRIVATE_KEY="verity_key.pem"
CERT="verity_cert.pem"
echo ${ROOTHASH} | tr -d '\n' > roothash.txt
openssl smime -sign -nocerts -noattr -binary -in roothash.txt \
    -inkey ${PRIVATE_KEY} -signer ${CERT} -outform der -out roothash.txt.signed

xxd -p sign.txt | tr -d '\n'

#This will show a hexdump of a signature that looks like:
# 3081f906092a864886f70d010702a081eb3081e8020101310f300d06096086480165030402010500300
# b06092a864886f70d0107013181c43081c1020101301b30163114301206035504030c0b6578616d706c65
# 2e636f6d020101300d06096086480165030402010500300d06092a864886f70d01010105000481806f196
# a3081d941d22de98b34fc56f5c1b7ffc827ccd1307be9017bb6773da49026ef556668185c68b30562a60c
# ec635bbe0a52ad92b878dac9e4ad146f5d36101b48ec5f522d12772b8e915524586598c8659494fba427e
# e46c02043f30f45e096a1b9a987fc200b815f43bb48d42ad2d64b3f632f5332e6f74890b4541b467d

Then from our target system boot arguments, we can append the new “root_hash_sig_hex” parameters:

setenv DATA_BLOCKS 16384
setenv DATA_BLOCK_SIZE 4096
setenv DATA_SECTORS 131072
setenv HASH_BLOCK_SIZE 4096
setenv HASH_ALG sha256
setenv SALT 2a4c7638f03b92bdb92d7284a742e0c4407c9ef65fdf2a7ea78ed02fde4a518b
setenv ROOT_HASH b96a69664f9279857931dbf64f942caf909076e40fd5bd5ed8d30b53ff922941
setenv DATA_DEV mmcblk0p1
setenv DATA_META_DEV mmcblk0p2
setenv VERITY_SIGNATURE 3081f906092a864886f70d010702a081eb3081e8020101310f300d06096086480165030402010500300
    b06092a864886f70d0107013181c43081c1020101301b30163114301206035504030c0b6578616d706c652e636f6d020101300d
    06096086480165030402010500300d06092a864886f70d010101050004818071fcaaf1b252a56448438e0a9350b7380a407b1e9
    0ae869ec5062466b0eb6cc5358e253a9d57086c358220745bc60c2a6d8dbc30c02fb1714c9c98f10e0679b87deb0c19929675c8
    fcf89f37c684f043583fca52729ffb6e928eb29b7ee0c9eab3a3b0809a4463f3c8d6d458745c9116a7df1677c707df6352f2323
    13a62ce20

setenv bootargs ${bootargs} dm-mod.create="verity,,,ro,0 \${DATA_SECTORS} verity 1 /dev/\${DATA_DEV}
    /dev/\${DATA_META_DEV} \${DATA_BLOCK_SIZE} \${HASH_BLOCK_SIZE} \${DATA_BLOCKS} 1 \${HASH_ALG}
    \${ROOT_HASH} \${SALT} 3 ignore_zero_blocks root_hash_sig_hex \${VERITY_SIGNATURE}"
    root=/dev/dm-0 dm_verity.require_signatures=1

Now you’re really done! Your kernel will verify the DM-Verity file system’s roothash and boot without any requirement for an initramfs.

 

Other Considerations

1) Here are all of the kernel config options which are related to device mapping and verity:

CONFIG_BLK_DEV=y
CONFIG_BLK_DEV_LOOP=y
CONFIG_MD=y
CONFIG_BLK_DEV_DM=y
CONFIG_BLK_DEV_MD=y
CONFIG_DM_INIT=y
CONFIG_DM_VERITY=y
CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y
CONFIG_SYSTEM_TRUSTED_KEYRING=y
CONFIG_SYSTEM_TRUSTED_KEYS="verity_cert.pem"

2) Your verified file system is only as good as your chain of trust. Every stage in your boot process needs to be properly secured. Our secure boot article outlines this in more detail here.

3) If your U-Boot environment can be tampered with, such that dm_verity.require_signatures can be disabled, then your root file system verification can easily be defeated. You might consider forcefully enabling this with a kernel patch as such:

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Wed, 30 Mar 2022 10:58:20 -0400
Subject: [PATCH 1/1] DM-Verity: Require dm-verity roothash signatures, with no
 ability to disable via /proc/cmdline

---
 drivers/md/dm-verity-verify-sig.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/drivers/md/dm-verity-verify-sig.c b/drivers/md/dm-verity-verify-sig.c
index 906e5a2034b5..b979e8e6b498 100644
--- a/drivers/md/dm-verity-verify-sig.c
+++ b/drivers/md/dm-verity-verify-sig.c
@@ -14,10 +14,12 @@
 
 #define DM_VERITY_VERIFY_ERR(s) DM_VERITY_ROOT_HASH_VERIFICATION " " s
 
-static bool require_signatures;
+static bool require_signatures = true;
+/*
 module_param(require_signatures, bool, 0444);
 MODULE_PARM_DESC(require_signatures,
        "Verify the roothash of dm-verity hash tree");
+*/
 
 #define DM_VERITY_IS_SIG_FORCE_ENABLED() \
    (require_signatures != false)

4) Small amounts of data corruption can be automatically corrected with CONFIG_DM_VERITY_FEC. If you’re interested in this, consider enabling this configuration option as well.

5) While booting from an MMC device, I observed that the MMC partition enumeration was out of sync with the early device mapping driver’s device lookup, resulting in a race condition. That is, it would attempt to find the enumerated MMC partitions (mmcblk0p1, mmcblk0p2) before they were instantiated and fail. I fixed this with another patch:

From: Nathan Barrett-Morrison <nathan.morrison@timesys.com>
Date: Wed, 30 Mar 2022 10:54:01 -0400
Subject: [PATCH 1/3] DM-Verity: Wait up to 10 seconds for eMMC/SD partitions
 to show up before failing dm_get_device()

---
 drivers/md/dm-verity-target.c | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/drivers/md/dm-verity-target.c b/drivers/md/dm-verity-target.c
index 711f101447e3..3fb02167a590 100644
--- a/drivers/md/dm-verity-target.c
+++ b/drivers/md/dm-verity-target.c
@@ -18,6 +18,7 @@
 #include "dm-verity-verify-sig.h"
 #include <linux/module.h>
 #include <linux/reboot.h>
+#include <linux/delay.h>
 
 #define DM_MSG_PREFIX            "verity"
 
@@ -998,10 +999,17 @@ static int verity_ctr(struct dm_target *ti, unsigned argc, char **argv)
     }
     v->version = num;
 
-    r = dm_get_device(ti, argv[1], FMODE_READ, &v->data_dev);
-    if (r) {
-        ti->error = "Data device lookup failed";
-        goto bad;
+    //Wait up to 10 seconds for devices to become available --
+    //wait_for_device_probe() sort of handles this, but the eMMC/SD probe finishes
+    //and dm_get_device() fails before the eMMC/SD partitions are found
+    for(i = 0; i <= 100; i++){
+        r = dm_get_device(ti, argv[1], FMODE_READ, &v->data_dev);
+        if (r && i < 100) {
+            msleep_interruptible(100);
+        }else if(r){
+            ti->error = "Data device lookup failed";
+            goto bad;
+        }
     }
 
     r = dm_get_device(ti, argv[2], FMODE_READ, &v->hash_dev);

 

Boot Time Comparisons

Testing on a relatively slower ARM processor (ADSP-SC589, Single core 500Mhz ARMv7) shows some major improvements. Keep in mind that this processor is favorable for showing the time difference here, as it has a slower initramfs loading and unpacking time. Newer processors with faster eMMC, DDR, and cores will narrow this margin.

On this processor, there is a total boot time difference of 29.19 seconds.

Using initramfs Not using initramfs Difference
Total boot time 53.33 s 24.14 s 29.19 s
U-Boot: Load initramfs from MMC 2.88 s 0.00 s 2.88 s
U-Boot: Verify initramfs 4.07 s 0.00 s 4.07 s
U-Boot: Relocate initramfs 3.81 s 0.00 s 3.81 s
Linux: Unpack initramfs 18.26 s 0.00 s 18.26 s
Linux: Run initramfs, mount partition, start systemd 1.41 s 1.09 s 0.32 s

On a faster, multi-core ARMv8+ processor, I believe you would still see a significant 1-2+ second difference in boot time. Early device mapping can be a very important tool if you are trying to hit a boot time goal of under 10 seconds.

 

Help!

As always, we offer professional engineering services to help you with your project. Whether you wish for us to integrate our security feature implementation, called VigiShield, into your project or just need another set of hands to help with general embedded software engineering and security, we’re available to help!