Android 4.4 introduced a number of security enhancements, most notably SELinux in enforcing mode. One security feature that initially got some press attention, because it was presumably aiming to ‘end all custom firmware’, but hasn’t been described in much detail, is verified boot. This post will briefly explain how verified boot works and then show how to configure and enable it on a Nexus device.
Verified boot with dm-verity
[Image from Android dm-verity documentation, licensed under Creative Commons Attribution 2.5]
CONFIG_DM_VERITYkernel configuration item. Like Chrome OS, Android 4.4 also uses the kernel’s dm-verity target, but the cryptographic verification of the root hash and mounting of verified partitions are implemented differently from Chrome OS.
The RSA public key used for verification is embedded in the boot partition under the verity_key filename and is used to verify the dm-verity mapping table. This mapping table holds the locations of the target device and the offset of the hash table, as well as the root hash and salt. The mapping table and its signature are part of the verity metablock which is written to disk directly after the last filesystem block of the target device. A partition is marked as verifiable by adding the verify flag to the Android-specific fs_mgr flags filed of the device’s fstab file. When Android’s filesystem manager encounters the verify flag in fstab, it loads the verity metadata from the block device specified in fstab and verifies its signature using the verity_key. If the signature check succeeds, the filesystem manager parses the dm-verity mapping table and passes it to the Linux device-mapper, which use the information contained in the mapping table in order to create a virtual dm-verity block device. This virtual block device is then mounted at the mount point specified in fstab in place of the corresponding physical device. As a result, all reads from the underlying physical device are transparently verified against the pre-generated hash tree. Modifying or adding files, or even remounting the partition in read-write mode, results in an integrity verification failure and an I/O error.
We must note that as dm-verity is a kernel feature, in order for the integrity protection it provides to be effective, the kernel the device boots needs to be trusted. On Android, this means verifying the boot partition, which also includes the root filesystem RAM disk (initrd) and the verity public key. This process is device-specific and is typically implemented in the device bootloader, usually by using an unmodifiable verification key stored in hardware to verify the boot partition’s signature.
Enabling verified boot
- Generate a hash tree for that system partition.
- Build a dm-verity table for that hash tree.
- Sign that dm-verity table to produce a table signature.
- Bundle the table signature and dm-verity table into verity metadata.
- Write the verity metadata and the hash tree to the system parition.
- Enable verified boot in the devices’s fstab file.
off_tdata type, which is 32-bit in current versions of Android’s bionic library.
BOARD_SYSTEMIMAGE_PARTITION_SIZEvalue in your device’s
BoardConfig.mkto allow for storing verity data. After you have adjusted the size of the system partition, transfer the veritysetup binary to the cache or data partitions of the device, and boot a recovery that allows root shell access over ADB. To generate and write the hash tree to the device we use the veritysetup format command as shown below.
# veritysetup --debug --hash-offset 838893568 --data-blocks 204800 format
# Updating VERITY header of size 512 on device /dev/block/mmcblk0p21, offset 838893568.
VERITY header information for /dev/block/mmcblk0p21
Hash type: 1
Data blocks: 204800
Data block size: 4096
Hash block size: 4096
Hash algorithm: sha256
Root hash: 5f061f591b51bf541ab9d89652ec543ba253f2ed9c8521ac61f1208267c3bfb1
This example was executed on a Nexus 4, make sure you use the correct block device for your phone instead of /dev/block/mmcblk0p21. The –hash-offset parameter is needed because we are writing the hash tree to the same device that holds filesystem data. It is specified in bytes (not blocks) and needs to point to a location after the verity metadata block. Adjust according to your filesystem size so that hash_offset > filesystem_size + 32k. The next parameter, –data-blocks, specifies the number of blocks used by the filesystem. The default block size is 4096, but you can specify a different size using the –data-block-size parameter. This value needs to match the size allocated to the filesystem with
BOARD_SYSTEMIMAGE_PARTITION_SIZE. If the command succeeds it will output the calculated root hash and the salt value used, as shown above. Everything but the root hash is saved in the superblock (first block) of the hash table. Make sure you save the root hash, as it is required to complete the verity setup.
Once you have the root hash and salt, you can generate and sign the dm-verity table. The table is a single line that contains the name of the block device, block sizes, offsets, salt and root hash values. You can use the gentable.py script (edit constant values accordingly first) to generate it or write it manually based on the output of veritysetup. See dm-verity’s documentation for details about the format. For our example it looks like this (single line, split for readability):
1 /dev/block/mmcblk0p21 /dev/block/mmcblk0p21 4096 4096 204800 204809 sha256
Next, generate a 2048-bit RSA key and sign the table using OpenSSL. You can use the command bellow or the sign.sh script on Github.
$ openssl dgst -sha1 -sign verity-key.pem -out table.sig table.bin
Once you have a signature you can generate the verity metadata block, which includes a magic number (
0xb001b001) and the metadata format version, followed by the RSA PKCS#1.5 signature blob and table string, padded with zeros to 32k. You can generate the metadata block with the mkverity.py script by passing the signature and table files like this:
$ ./mkverity.py table.sig table.bin verity.bin
Next, write the generated verity.bin file to the system partition using dd or a similar tool, right after the last filesystem block and before the start of the verity hash table. Using the same number of data blocks passed to veritysetup, the needed command (which also needs to be executed in recovery) becomes:
# dd if=verity.bin of=/dev/block/mmcblk0p21 bs=4096 seek=204800
# veritysetup --debug --hash-offset 838893568 --data-blocks 204800 verify
If verification succeeds, reboot the device and verify that the device boots without errors. If it does, you can proceed to the next step: add the verification key to the boot image and enable automatic integrity verification.
The RSA public key used for verification needs to be in mincrypt format (also used by the stock recovery when verifying OTA file signatures), which is a serialization of mincrypt’s
RSAPublicKey structure. The interesting thing about this structure is that ts doesn’t simply include the modulus and public exponent values, but contains pre-computed values used by mincrypt’s RSA implementation (based on Montgomery reduction). Therefore converting an OpenSSL RSA public key to mincrypt format requires some modular operations and is not simply a binary format conversion. You can convert the PEM key using the pem2mincrypt tool (conversion code shamelessly stolen from secure adb‘s implementation). Once you have converted the key, include it in the root of your boot image under the verity_key filename. The last step is to modify the device’s fstab file in order to enable block integrity verification for the system partition. This is simply a matter of adding the verify flag, as shown below:
/dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro, barrier=1 wait,verify
Next, verify that your kernel configuration enable
CONFIG_DM_VERITY, enable it if needed and build your boot image. Once you have boot.img, you can try booting the device with it using fastboot boot boot.img (without flashing it). If the hash table and verity metadata blcok have been generated and written correctly, the device should boot, and /system should be a mount of the automatically created device-mapper virtual device, as shown below. If the boot is successful, you can permanently flash the boot image to the device.
# mount|grep system
/dev/block/dm-0 /system ext4 ro,seclabel,relatime,data=ordered 0 0
Now any modifications to the system partition will result in read errors when reading the corresponding file(s). Unfortunately, system modifications by file-based OTA updates, which modify file blocks without updating verity metadata, will also invalidate the hash tree. As mentioned in the official documentation, in order to be compatible with dm-verity verified boot, OTA updates should also operate at the block level, ensuring that both file blocks and the hash tree and metadata are updated. This requires changing the current OTA update infrastructure, which is probably one of the reasons verified boot hasn’t been deployed to production devices yet.
Android includes a verified boot implementation based on the dm-verity device-mapper target since version 4.4. dm-verity is enabled by adding a hash table and a signed metadata block to the system partition and specifying the verify flag in the device’s fstab file. At boot time Android verifies the metadata signature and uses the included device-mapper table to create and mount a virtual block device at /system. As a result, all reads from /system are verified against the dm-verity hash tree, and any modification to the system partition results in I/O errors.