| Verified Boot on the Beaglebone Black |
| ===================================== |
| |
| Introduction |
| ------------ |
| |
| Before reading this, please read verified-boot.txt and signature.txt. These |
| instructions are for mainline U-Boot from v2014.07 onwards. |
| |
| There is quite a bit of documentation in this directory describing how |
| verified boot works in U-Boot. There is also a test which runs through the |
| entire process of signing an image and running U-Boot (sandbox) to check it. |
| However, it might be useful to also have an example on a real board. |
| |
| Beaglebone Black is a fairly common board so seems to be a reasonable choice |
| for an example of how to enable verified boot using U-Boot. |
| |
| First a note that may to help avoid confusion. U-Boot and Linux both use |
| device tree. They may use the same device tree source, but it is seldom useful |
| for them to use the exact same binary from the same place. More typically, |
| U-Boot has its device tree packaged wtih it, and the kernel's device tree is |
| packaged with the kernel. In particular this is important with verified boot, |
| since U-Boot's device tree must be immutable. If it can be changed then the |
| public keys can be changed and verified boot is useless. An attacker can |
| simply generate a new key and put his public key into U-Boot so that |
| everything verifies. On the other hand the kernel's device tree typically |
| changes when the kernel changes, so it is useful to package an updated device |
| tree with the kernel binary. U-Boot supports the latter with its flexible FIT |
| format (Flat Image Tree). |
| |
| |
| Overview |
| -------- |
| |
| The steps are roughly as follows: |
| |
| 1. Build U-Boot for the board, with the verified boot options enabled. |
| |
| 2. Obtain a suitable Linux kernel |
| |
| 3. Create a Image Tree Source file (ITS) file describing how you want the |
| kernel to be packaged, compressed and signed. |
| |
| 4. Create a key pair |
| |
| 5. Sign the kernel |
| |
| 6. Put the public key into U-Boot's image |
| |
| 7. Put U-Boot and the kernel onto the board |
| |
| 8. Try it |
| |
| |
| Step 1: Build U-Boot |
| -------------------- |
| |
| a. Set up the environment variable to point to your toolchain. You will need |
| this for U-Boot and also for the kernel if you build it. For example if you |
| installed a Linaro version manually it might be something like: |
| |
| export CROSS_COMPILE=/opt/linaro/gcc-linaro-arm-linux-gnueabihf-4.8-2013.08_linux/bin/arm-linux-gnueabihf- |
| |
| or if you just installed gcc-arm-linux-gnueabi then it might be |
| |
| export CROSS_COMPILE=arm-linux-gnueabi- |
| |
| b. Configure and build U-Boot with verified boot enabled: |
| |
| export ARCH=arm |
| export UBOOT=/path/to/u-boot |
| cd $UBOOT |
| # You can add -j10 if you have 10 CPUs to make it faster |
| make O=b/am335x_boneblack_vboot am335x_boneblack_vboot_config all |
| export UOUT=$UBOOT/b/am335x_boneblack_vboot |
| |
| c. You will now have a U-Boot image: |
| |
| file b/am335x_boneblack_vboot/u-boot-dtb.img |
| b/am335x_boneblack_vboot/u-boot-dtb.img: u-boot legacy uImage, U-Boot 2014.07-rc2-00065-g2f69f8, Firmware/ARM, Firmware Image (Not compressed), 395375 bytes, Sat May 31 16:19:04 2014, Load Address: 0x80800000, Entry Point: 0x00000000, Header CRC: 0x0ABD6ACA, Data CRC: 0x36DEF7E4 |
| |
| |
| Step 2: Build Linux |
| -------------------- |
| |
| a. Find the kernel image ('Image') and device tree (.dtb) file you plan to |
| use. In our case it is am335x-boneblack.dtb and it is built with the kernel. |
| At the time of writing an SD Boot image can be obtained from here: |
| |
| http://www.elinux.org/Beagleboard:Updating_The_Software#Image_For_Booting_From_microSD |
| |
| You can write this to an SD card and then mount it to extract the kernel and |
| device tree files. |
| |
| You can also build a kernel. Instructions for this are are here: |
| |
| http://elinux.org/Building_BBB_Kernel |
| |
| or you can use your favourite search engine. Following these instructions |
| produces a kernel Image and device tree files. For the record the steps were: |
| |
| export KERNEL=/path/to/kernel |
| cd $KERNEL |
| git clone git://github.com/beagleboard/kernel.git . |
| git checkout v3.14 |
| ./patch.sh |
| cp configs/beaglebone kernel/arch/arm/configs/beaglebone_defconfig |
| cd kernel |
| make beaglebone_defconfig |
| make uImage dtbs # -j10 if you have 10 CPUs |
| export OKERNEL=$KERNEL/kernel/arch/arm/boot |
| |
| c. You now have the 'Image' and 'am335x-boneblack.dtb' files needed to boot. |
| |
| |
| Step 3: Create the ITS |
| ---------------------- |
| |
| Set up a directory for your work. |
| |
| export WORK=/path/to/dir |
| cd $WORK |
| |
| Put this into a file in that directory called sign.its: |
| |
| /dts-v1/; |
| |
| / { |
| description = "Beaglebone black"; |
| #address-cells = <1>; |
| |
| images { |
| kernel@1 { |
| data = /incbin/("Image.lzo"); |
| type = "kernel"; |
| arch = "arm"; |
| os = "linux"; |
| compression = "lzo"; |
| load = <0x80008000>; |
| entry = <0x80008000>; |
| hash@1 { |
| algo = "sha1"; |
| }; |
| }; |
| fdt@1 { |
| description = "beaglebone-black"; |
| data = /incbin/("am335x-boneblack.dtb"); |
| type = "flat_dt"; |
| arch = "arm"; |
| compression = "none"; |
| hash@1 { |
| algo = "sha1"; |
| }; |
| }; |
| }; |
| configurations { |
| default = "conf@1"; |
| conf@1 { |
| kernel = "kernel@1"; |
| fdt = "fdt@1"; |
| signature@1 { |
| algo = "sha1,rsa2048"; |
| key-name-hint = "dev"; |
| sign-images = "fdt", "kernel"; |
| }; |
| }; |
| }; |
| }; |
| |
| |
| The explanation for this is all in the documentation you have already read. |
| But briefly it packages a kernel and device tree, and provides a single |
| configuration to be signed with a key named 'dev'. The kernel is compressed |
| with LZO to make it smaller. |
| |
| |
| Step 4: Create a key pair |
| ------------------------- |
| |
| See signature.txt for details on this step. |
| |
| cd $WORK |
| mkdir keys |
| openssl genrsa -F4 -out keys/dev.key 2048 |
| openssl req -batch -new -x509 -key keys/dev.key -out keys/dev.crt |
| |
| Note: keys/dev.key contains your private key and is very secret. If anyone |
| gets access to that file they can sign kernels with it. Keep it secure. |
| |
| |
| Step 5: Sign the kernel |
| ----------------------- |
| |
| We need to use mkimage (which was built when you built U-Boot) to package the |
| Linux kernel into a FIT (Flat Image Tree, a flexible file format that U-Boot |
| can load) using the ITS file you just created. |
| |
| At the same time we must put the public key into U-Boot device tree, with the |
| 'required' property, which tells U-Boot that this key must be verified for the |
| image to be valid. You will make this key available to U-Boot for booting in |
| step 6. |
| |
| ln -s $OKERNEL/dts/am335x-boneblack.dtb |
| ln -s $OKERNEL/Image |
| ln -s $UOUT/u-boot-dtb.img |
| cp $UOUT/arch/arm/dts/am335x-boneblack.dtb am335x-boneblack-pubkey.dtb |
| lzop Image |
| $UOUT/tools/mkimage -f sign.its -K am335x-boneblack-pubkey.dtb -k keys -r image.fit |
| |
| You should see something like this: |
| |
| FIT description: Beaglebone black |
| Created: Sun Jun 1 12:50:30 2014 |
| Image 0 (kernel@1) |
| Description: unavailable |
| Created: Sun Jun 1 12:50:30 2014 |
| Type: Kernel Image |
| Compression: lzo compressed |
| Data Size: 7790938 Bytes = 7608.34 kB = 7.43 MB |
| Architecture: ARM |
| OS: Linux |
| Load Address: 0x80008000 |
| Entry Point: 0x80008000 |
| Hash algo: sha1 |
| Hash value: c94364646427e10f423837e559898ef02c97b988 |
| Image 1 (fdt@1) |
| Description: beaglebone-black |
| Created: Sun Jun 1 12:50:30 2014 |
| Type: Flat Device Tree |
| Compression: uncompressed |
| Data Size: 31547 Bytes = 30.81 kB = 0.03 MB |
| Architecture: ARM |
| Hash algo: sha1 |
| Hash value: cb09202f889d824f23b8e4404b781be5ad38a68d |
| Default Configuration: 'conf@1' |
| Configuration 0 (conf@1) |
| Description: unavailable |
| Kernel: kernel@1 |
| FDT: fdt@1 |
| |
| |
| Now am335x-boneblack-pubkey.dtb contains the public key and image.fit contains |
| the signed kernel. Jump to step 6 if you like, or continue reading to increase |
| your understanding. |
| |
| You can also run fit_check_sign to check it: |
| |
| $UOUT/tools/fit_check_sign -f image.fit -k am335x-boneblack-pubkey.dtb |
| |
| which results in: |
| |
| Verifying Hash Integrity ... sha1,rsa2048:dev+ |
| ## Loading kernel from FIT Image at 7fc6ee469000 ... |
| Using 'conf@1' configuration |
| Verifying Hash Integrity ... |
| sha1,rsa2048:dev+ |
| OK |
| |
| Trying 'kernel@1' kernel subimage |
| Description: unavailable |
| Created: Sun Jun 1 12:50:30 2014 |
| Type: Kernel Image |
| Compression: lzo compressed |
| Data Size: 7790938 Bytes = 7608.34 kB = 7.43 MB |
| Architecture: ARM |
| OS: Linux |
| Load Address: 0x80008000 |
| Entry Point: 0x80008000 |
| Hash algo: sha1 |
| Hash value: c94364646427e10f423837e559898ef02c97b988 |
| Verifying Hash Integrity ... |
| sha1+ |
| OK |
| |
| Unimplemented compression type 4 |
| ## Loading fdt from FIT Image at 7fc6ee469000 ... |
| Using 'conf@1' configuration |
| Trying 'fdt@1' fdt subimage |
| Description: beaglebone-black |
| Created: Sun Jun 1 12:50:30 2014 |
| Type: Flat Device Tree |
| Compression: uncompressed |
| Data Size: 31547 Bytes = 30.81 kB = 0.03 MB |
| Architecture: ARM |
| Hash algo: sha1 |
| Hash value: cb09202f889d824f23b8e4404b781be5ad38a68d |
| Verifying Hash Integrity ... |
| sha1+ |
| OK |
| |
| Loading Flat Device Tree ... OK |
| |
| ## Loading ramdisk from FIT Image at 7fc6ee469000 ... |
| Using 'conf@1' configuration |
| Could not find subimage node |
| |
| Signature check OK |
| |
| |
| At the top, you see "sha1,rsa2048:dev+". This means that it checked an RSA key |
| of size 2048 bits using SHA1 as the hash algorithm. The key name checked was |
| 'dev' and the '+' means that it verified. If it showed '-' that would be bad. |
| |
| Once the configuration is verified it is then possible to rely on the hashes |
| in each image referenced by that configuration. So fit_check_sign goes on to |
| load each of the images. We have a kernel and an FDT but no ramkdisk. In each |
| case fit_check_sign checks the hash and prints sha1+ meaning that the SHA1 |
| hash verified. This means that none of the images has been tampered with. |
| |
| There is a test in test/vboot which uses U-Boot's sandbox build to verify that |
| the above flow works. |
| |
| But it is fun to do this by hand, so you can load image.fit into a hex editor |
| like ghex, and change a byte in the kernel: |
| |
| $UOUT/tools/fit_info -f image.fit -n /images/kernel@1 -p data |
| NAME: kernel@1 |
| LEN: 7790938 |
| OFF: 168 |
| |
| This tells us that the kernel starts at byte offset 168 (decimal) in image.fit |
| and extends for about 7MB. Try changing a byte at 0x2000 (say) and run |
| fit_check_sign again. You should see something like: |
| |
| Verifying Hash Integrity ... sha1,rsa2048:dev+ |
| ## Loading kernel from FIT Image at 7f5a39571000 ... |
| Using 'conf@1' configuration |
| Verifying Hash Integrity ... |
| sha1,rsa2048:dev+ |
| OK |
| |
| Trying 'kernel@1' kernel subimage |
| Description: unavailable |
| Created: Sun Jun 1 13:09:21 2014 |
| Type: Kernel Image |
| Compression: lzo compressed |
| Data Size: 7790938 Bytes = 7608.34 kB = 7.43 MB |
| Architecture: ARM |
| OS: Linux |
| Load Address: 0x80008000 |
| Entry Point: 0x80008000 |
| Hash algo: sha1 |
| Hash value: c94364646427e10f423837e559898ef02c97b988 |
| Verifying Hash Integrity ... |
| sha1 error |
| Bad hash value for 'hash@1' hash node in 'kernel@1' image node |
| Bad Data Hash |
| |
| ## Loading fdt from FIT Image at 7f5a39571000 ... |
| Using 'conf@1' configuration |
| Trying 'fdt@1' fdt subimage |
| Description: beaglebone-black |
| Created: Sun Jun 1 13:09:21 2014 |
| Type: Flat Device Tree |
| Compression: uncompressed |
| Data Size: 31547 Bytes = 30.81 kB = 0.03 MB |
| Architecture: ARM |
| Hash algo: sha1 |
| Hash value: cb09202f889d824f23b8e4404b781be5ad38a68d |
| Verifying Hash Integrity ... |
| sha1+ |
| OK |
| |
| Loading Flat Device Tree ... OK |
| |
| ## Loading ramdisk from FIT Image at 7f5a39571000 ... |
| Using 'conf@1' configuration |
| Could not find subimage node |
| |
| Signature check Bad (error 1) |
| |
| |
| It has detected the change in the kernel. |
| |
| You can also be sneaky and try to switch images, using the libfdt utilities |
| that come with dtc (package name is device-tree-compiler but you will need a |
| recent version like 1.4: |
| |
| dtc -v |
| Version: DTC 1.4.0 |
| |
| First we can check which nodes are actually hashed by the configuration: |
| |
| fdtget -l image.fit / |
| images |
| configurations |
| |
| fdtget -l image.fit /configurations |
| conf@1 |
| fdtget -l image.fit /configurations/conf@1 |
| signature@1 |
| |
| fdtget -p image.fit /configurations/conf@1/signature@1 |
| hashed-strings |
| hashed-nodes |
| timestamp |
| signer-version |
| signer-name |
| value |
| algo |
| key-name-hint |
| sign-images |
| |
| fdtget image.fit /configurations/conf@1/signature@1 hashed-nodes |
| / /configurations/conf@1 /images/fdt@1 /images/fdt@1/hash@1 /images/kernel@1 /images/kernel@1/hash@1 |
| |
| This gives us a bit of a look into the signature that mkimage added. Note you |
| can also use fdtdump to list the entire device tree. |
| |
| Say we want to change the kernel that this configuration uses |
| (/images/kernel@1). We could just put a new kernel in the image, but we will |
| need to change the hash to match. Let's simulate that by changing a byte of |
| the hash: |
| |
| fdtget -tx image.fit /images/kernel@1/hash@1 value |
| c9436464 6427e10f 423837e5 59898ef0 2c97b988 |
| fdtput -tx image.fit /images/kernel@1/hash@1 value c9436464 6427e10f 423837e5 59898ef0 2c97b981 |
| |
| Now check it again: |
| |
| $UOUT/tools/fit_check_sign -f image.fit -k am335x-boneblack-pubkey.dtb |
| Verifying Hash Integrity ... sha1,rsa2048:devrsa_verify_with_keynode: RSA failed to verify: -13 |
| rsa_verify_with_keynode: RSA failed to verify: -13 |
| - |
| Failed to verify required signature 'key-dev' |
| Signature check Bad (error 1) |
| |
| This time we don't even get as far as checking the images, since the |
| configuration signature doesn't match. We can't change any hashes without the |
| signature check noticing. The configuration is essentially locked. U-Boot has |
| a public key for which it requires a match, and will not permit the use of any |
| configuration that does not match that public key. The only way the |
| configuration will match is if it was signed by the matching private key. |
| |
| It would also be possible to add a new signature node that does match your new |
| configuration. But that won't work since you are not allowed to change the |
| configuration in any way. Try it with a fresh (valid) image if you like by |
| running the mkimage link again. Then: |
| |
| fdtput -p image.fit /configurations/conf@1/signature@2 value fred |
| $UOUT/tools/fit_check_sign -f image.fit -k am335x-boneblack-pubkey.dtb |
| Verifying Hash Integrity ... - |
| sha1,rsa2048:devrsa_verify_with_keynode: RSA failed to verify: -13 |
| rsa_verify_with_keynode: RSA failed to verify: -13 |
| - |
| Failed to verify required signature 'key-dev' |
| Signature check Bad (error 1) |
| |
| |
| Of course it would be possible to add an entirely new configuration and boot |
| with that, but it still needs to be signed, so it won't help. |
| |
| |
| 6. Put the public key into U-Boot's image |
| ----------------------------------------- |
| |
| Having confirmed that the signature is doing its job, let's try it out in |
| U-Boot on the board. U-Boot needs access to the public key corresponding to |
| the private key that you signed with so that it can verify any kernels that |
| you sign. |
| |
| cd $UBOOT |
| make O=b/am335x_boneblack_vboot EXT_DTB=${WORK}/am335x-boneblack-pubkey.dtb |
| |
| Here we are overrriding the normal device tree file with our one, which |
| contains the public key. |
| |
| Now you have a special U-Boot image with the public key. It can verify can |
| kernel that you sign with the private key as in step 5. |
| |
| If you like you can take a look at the public key information that mkimage |
| added to U-Boot's device tree: |
| |
| fdtget -p am335x-boneblack-pubkey.dtb /signature/key-dev |
| required |
| algo |
| rsa,r-squared |
| rsa,modulus |
| rsa,n0-inverse |
| rsa,num-bits |
| key-name-hint |
| |
| This has information about the key and some pre-processed values which U-Boot |
| can use to verify against it. These values are obtained from the public key |
| certificate by mkimage, but require quite a bit of code to generate. To save |
| code space in U-Boot, the information is extracted and written in raw form for |
| U-Boot to easily use. The same mechanism is used in Google's Chrome OS. |
| |
| Notice the 'required' property. This marks the key as required - U-Boot will |
| not boot any image that does not verify against this key. |
| |
| |
| 7. Put U-Boot and the kernel onto the board |
| ------------------------------------------- |
| |
| The method here varies depending on how you are booting. For this example we |
| are booting from an micro-SD card with two partitions, one for U-Boot and one |
| for Linux. Put it into your machine and write U-Boot and the kernel to it. |
| Here the card is /dev/sde: |
| |
| cd $WORK |
| export UDEV=/dev/sde1 # Change thes two lines to the correct device |
| export KDEV=/dev/sde2 |
| sudo mount $UDEV /mnt/tmp && sudo cp $UOUT/u-boot-dtb.img /mnt/tmp/u-boot.img && sleep 1 && sudo umount $UDEV |
| sudo mount $KDEV /mnt/tmp && sudo cp $WORK/image.fit /mnt/tmp/boot/image.fit && sleep 1 && sudo umount $KDEV |
| |
| |
| 8. Try it |
| --------- |
| |
| Boot the board using the commands below: |
| |
| setenv bootargs console=ttyO0,115200n8 quiet root=/dev/mmcblk0p2 ro rootfstype=ext4 rootwait |
| ext2load mmc 0:2 82000000 /boot/image.fit |
| bootm 82000000 |
| |
| You should then see something like this: |
| |
| U-Boot# setenv bootargs console=ttyO0,115200n8 quiet root=/dev/mmcblk0p2 ro rootfstype=ext4 rootwait |
| U-Boot# ext2load mmc 0:2 82000000 /boot/image.fit |
| 7824930 bytes read in 589 ms (12.7 MiB/s) |
| U-Boot# bootm 82000000 |
| ## Loading kernel from FIT Image at 82000000 ... |
| Using 'conf@1' configuration |
| Verifying Hash Integrity ... sha1,rsa2048:dev+ OK |
| Trying 'kernel@1' kernel subimage |
| Description: unavailable |
| Created: 2014-06-01 19:32:54 UTC |
| Type: Kernel Image |
| Compression: lzo compressed |
| Data Start: 0x820000a8 |
| Data Size: 7790938 Bytes = 7.4 MiB |
| Architecture: ARM |
| OS: Linux |
| Load Address: 0x80008000 |
| Entry Point: 0x80008000 |
| Hash algo: sha1 |
| Hash value: c94364646427e10f423837e559898ef02c97b988 |
| Verifying Hash Integrity ... sha1+ OK |
| ## Loading fdt from FIT Image at 82000000 ... |
| Using 'conf@1' configuration |
| Trying 'fdt@1' fdt subimage |
| Description: beaglebone-black |
| Created: 2014-06-01 19:32:54 UTC |
| Type: Flat Device Tree |
| Compression: uncompressed |
| Data Start: 0x8276e2ec |
| Data Size: 31547 Bytes = 30.8 KiB |
| Architecture: ARM |
| Hash algo: sha1 |
| Hash value: cb09202f889d824f23b8e4404b781be5ad38a68d |
| Verifying Hash Integrity ... sha1+ OK |
| Booting using the fdt blob at 0x8276e2ec |
| Uncompressing Kernel Image ... OK |
| Loading Device Tree to 8fff5000, end 8ffffb3a ... OK |
| |
| Starting kernel ... |
| |
| [ 0.582377] omap_init_mbox: hwmod doesn't have valid attrs |
| [ 2.589651] musb-hdrc musb-hdrc.0.auto: Failed to request rx1. |
| [ 2.595830] musb-hdrc musb-hdrc.0.auto: musb_init_controller failed with status -517 |
| [ 2.606470] musb-hdrc musb-hdrc.1.auto: Failed to request rx1. |
| [ 2.612723] musb-hdrc musb-hdrc.1.auto: musb_init_controller failed with status -517 |
| [ 2.940808] drivers/rtc/hctosys.c: unable to open rtc device (rtc0) |
| [ 7.248889] libphy: PHY 4a101000.mdio:01 not found |
| [ 7.253995] net eth0: phy 4a101000.mdio:01 not found on slave 1 |
| systemd-fsck[83]: Angstrom: clean, 50607/218160 files, 306348/872448 blocks |
| |
| .---O---. |
| | | .-. o o |
| | | |-----.-----.-----.| | .----..-----.-----. |
| | | | __ | ---'| '--.| .-'| | | |
| | | | | | |--- || --'| | | ' | | | | |
| '---'---'--'--'--. |-----''----''--' '-----'-'-'-' |
| -' | |
| '---' |
| |
| The Angstrom Distribution beaglebone ttyO0 |
| |
| Angstrom v2012.12 - Kernel 3.14.1+ |
| |
| beaglebone login: |
| |
| At this point your kernel has been verified and you can be sure that it is one |
| that you signed. As an exercise, try changing image.fit as in step 5 and see |
| what happens. |
| |
| |
| Further Improvements |
| -------------------- |
| |
| Several of the steps here can be easily automated. In particular it would be |
| capital if signing and packaging a kernel were easy, perhaps a simple make |
| target in the kernel. |
| |
| Some mention of how to use multiple .dtb files in a FIT might be useful. |
| |
| U-Boot's verified boot mechanism has not had a robust and independent security |
| review. Such a review should look at the implementation and its resistance to |
| attacks. |
| |
| Perhaps the verified boot feature could could be integrated into the Amstrom |
| distribution. |
| |
| |
| Simon Glass |
| sjg@chromium.org |
| 2-June-14 |