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!
where did you send that patch to?
I can’t find it in the email archives.
Any idea when/if this will be upstreamed?
Thanks!
I didn’t get around to attempting to upstream the patches. I’m not sure how well they would currently apply on 6.x
Question regarding verification of dm-verity:
1. It is not clear to me if the complete roothash of the ext4 rootfs is calculated at boot time and matched with the stored roothash? Is this being done?
I can see that the algorithm, digest and salt is being provided using kernel command-line parameters.
2. At a later stage(after boot), when the rootfs page is accessed(read), does the kernel params here passed during boot are still available? Are these params used for verifying the rootfs blocks in any way?
The following page explains how the hash_dev is used to verify the integrity of rootfs : https://www.starlab.io/blog/dm-verity-in-embedded-device-security
I believe this is without any kernel command-line parameters.
1) I believe the dm-verity metadata contains the entire hash tree. The roothash is just used to verify that this metadata/hash tree is valid, correct, and allowed to be used for hash comparisons. Files/blocks themselves don’t actually get checked until they’re used, otherwise it would take quite awhile for the kernel to boot and verify every single file/block on the partition.
2) Kernel command line params can be found after booting at /proc/cmdline. However, I believe the dm-verity early mapper params I’ve discussed here would be passed into the verity subsystem once and then they would not be used directly from /proc/cmdline afterwards.
Hi Nathan,
This article is really informative. I am also planning to enable dm-verity for secure boot implement.
Currently, I am working on Kernel 4.1.15. Where I am not finding above mentioned source files “dm-verity-verify-sig.c”, “drivers/md/dm-verity-target.c” etc.. These files are available from kernel source 5.x.x.
I want to opt “early device mapping” way only but not sure its do able on current kernel 4.1.15.
Please confirm..
Regards,
CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG (and therefore dm-verity-verify-sig.c) was not added until 5.4, so there’s definitely no root hash verification support before 5.4. You may not necessarily need root hash verification support though. If your bootloader is already verifying this roothash through your chain of trust and hardcodes the value (i.e.: CONFIG_ENV_IS_NOWHERE), you may be fine without it. This would require a security audit to be sure of.
Verity itself was available starting in 3.4. in 4.1.15 this happens to reside at drivers/md/dm-verity.c.
– Nathan
Hi Nathan, do you have experience regarding the size of the verity hash images? I expect the image to grow in terms of N*LOG(N), where N is the number of blocks of the partition, for example rootfs. In my project OpenCritis (https://gitlab.com/opencritis/opencritis) I assume 4MB will be sufficient for rootfs partitions of size 35MB up to 500MB. Any comments are welcome. Regards, frehberg
Hi Frehberg,
I tried to figure this out once. My conclusion was that:
HASH_TABLE_SIZE = SHA256_SIZE * ((ROOTFS_SIZE/DATA_BLOCK_SIZE)-1)
Say you have a filesystem with 8 4K blocks (32KB)… your hash table is going to look like:
So, that would be 7 * 32 byte hashes = 224 bytes
Feed that back into my expression above:
HASH_TABLE_SIZE = 32 * ((32768 / 4096)-1) = 224 bytes
On top of this, there’s also a Verity metadata header (which can be omitted — but you have to pass the parameters manually). If you don’t omit this, that’s an additional 512 bytes
– Nathan
HI, as for step “5)” , maybe kernel command line “rootwait” might solve the issue? without the need for the additional patch to wait N millis for the mapper device to be mounted?
It may vary on your kernel version and drivers. On the one we tested, rootwait (and rootdelay) did not matter.
rootwait adds a dependency between mounting the RFS and the storage driver, it unfortunately does not add any dependency chain between the early DM subsystem and storage driver. So you still end up with:
device-mapper: init: waiting for all devices to be available before creating mapped devices
device-mapper: table: 253:0: verity: Data device lookup failed
device-mapper: ioctl: error adding target to table
mmc_host mmc0: Bus speed (slot 0) = 50000000Hz (slot req 25000000Hz, actual 25000000HZ div = 1)
mmc0: new SDHC card at address 59b4
mmcblk0: mmc0:59b4 00000 7.35 GiB
mmcblk0: p1 p2
Where the early DM subsystem tried to use the storage device before the storage driver even probed.
– Nathan
cool page, but please add a note, that you use “rsa:1024” as otherwise uboot bootargs maxlength is exceeded and bootargs string might be cut off.
Very good post, many thanks, just in your bootargs expression the positions must be changed to
\${DATA_BLOCK_SIZE} \${HASH_BLOCK_SIZE}
Thank you Frank, this is a valid correction.